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,69 @@
1
+ """CORE schema and fingerprint helpers.
2
+
3
+ These helpers separate three concerns:
4
+ - operational_fingerprint: stable for replay and gating
5
+ - audit_fingerprint: stable for provenance and human inspection
6
+ - schema_fingerprint: stable for schema evolution control
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ from dataclasses import fields, is_dataclass
14
+ from typing import Any, get_type_hints
15
+
16
+ from core_runtime.core.numeric_normalization import quantize_for_hash
17
+
18
+
19
+ def operational_fingerprint(payload: dict[str, Any]) -> str:
20
+ """Fingerprint for replay and determinism gates."""
21
+ canonical = json.dumps(
22
+ quantize_for_hash(payload),
23
+ sort_keys=True,
24
+ separators=(",", ":"),
25
+ ensure_ascii=False,
26
+ default=str,
27
+ )
28
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
29
+
30
+
31
+ def audit_fingerprint(payload: dict[str, Any]) -> str:
32
+ """Fingerprint for provenance and audit reports."""
33
+ canonical = json.dumps(
34
+ payload,
35
+ sort_keys=True,
36
+ separators=(",", ":"),
37
+ ensure_ascii=False,
38
+ default=str,
39
+ )
40
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
41
+
42
+
43
+ def schema_fingerprint(cls: type[Any]) -> str:
44
+ """Fingerprint the declared schema of a dataclass or typed container."""
45
+ if is_dataclass(cls):
46
+ schema = [
47
+ {
48
+ "name": f.name,
49
+ "type": str(f.type),
50
+ "default": repr(f.default),
51
+ "default_factory": repr(getattr(f, "default_factory", None)),
52
+ }
53
+ for f in fields(cls)
54
+ ]
55
+ else:
56
+ hints = get_type_hints(cls)
57
+ schema = [
58
+ {"name": name, "type": str(tp)}
59
+ for name, tp in sorted(hints.items(), key=lambda item: item[0])
60
+ ]
61
+
62
+ canonical = json.dumps(
63
+ schema,
64
+ sort_keys=True,
65
+ separators=(",", ":"),
66
+ ensure_ascii=False,
67
+ default=str,
68
+ )
69
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
@@ -0,0 +1,548 @@
1
+ """Sensor evidence primitives for CORE v4.4 bootstrap.
2
+
3
+ This module defines deterministic, serializable records for future sensor
4
+ evidence integration. It does not implement live sensors and does not mutate
5
+ runtime state.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import csv
11
+ import hashlib
12
+ import json
13
+ from dataclasses import dataclass, field
14
+ from pathlib import Path
15
+ from typing import Any, Mapping
16
+
17
+
18
+ SENSOR_EVIDENCE_SCHEMA_VERSION = "core.sensor_evidence.v1"
19
+ SENSOR_TRACE_ENCODING = "core.sensor_trace.v1"
20
+ OBSERVATION_EVENT_ENCODING = "core.observation_event.v1"
21
+
22
+
23
+ def _canonical_json(value: Any) -> str:
24
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
25
+
26
+
27
+ def _sha256_text(value: str) -> str:
28
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
29
+
30
+
31
+ def _require_non_empty_string(value: Any, field_name: str) -> str:
32
+ if not isinstance(value, str):
33
+ raise TypeError(f"{field_name} must be a string")
34
+ normalized = value.strip()
35
+ if not normalized:
36
+ raise ValueError(f"{field_name} must not be empty")
37
+ return normalized
38
+
39
+
40
+ def _require_finite_float(value: Any, field_name: str) -> float:
41
+ number = float(value)
42
+ if number != number or number in (float("inf"), float("-inf")):
43
+ raise ValueError(f"{field_name} must be finite")
44
+ return number
45
+
46
+
47
+ def _sorted_strings(values: Mapping[str, Any] | list[Any] | tuple[Any, ...]) -> tuple[str, ...]:
48
+ collected: list[str] = []
49
+ if isinstance(values, Mapping):
50
+ iterable: list[Any] = list(values.values())
51
+ else:
52
+ iterable = list(values)
53
+ for item in iterable:
54
+ if isinstance(item, str) and item.strip():
55
+ collected.append(item.strip())
56
+ return tuple(sorted(dict.fromkeys(collected)))
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class SensorSource:
61
+ sensor_id: str
62
+ sensor_type: str
63
+ capture_mode: str
64
+ hardware_version: str | None = None
65
+ firmware_version: str | None = None
66
+ model_version: str | None = None
67
+ calibration_id: str | None = None
68
+ environment_id: str | None = None
69
+ metadata: Mapping[str, Any] = field(default_factory=dict)
70
+
71
+ def __post_init__(self) -> None:
72
+ object.__setattr__(self, "sensor_id", _require_non_empty_string(self.sensor_id, "sensor_id"))
73
+ object.__setattr__(self, "sensor_type", _require_non_empty_string(self.sensor_type, "sensor_type"))
74
+ object.__setattr__(self, "capture_mode", _require_non_empty_string(self.capture_mode, "capture_mode"))
75
+
76
+ def to_dict(self) -> dict[str, Any]:
77
+ return {
78
+ "schema_version": SENSOR_EVIDENCE_SCHEMA_VERSION,
79
+ "sensor_id": self.sensor_id,
80
+ "sensor_type": self.sensor_type,
81
+ "capture_mode": self.capture_mode,
82
+ "hardware_version": self.hardware_version,
83
+ "firmware_version": self.firmware_version,
84
+ "model_version": self.model_version,
85
+ "calibration_id": self.calibration_id,
86
+ "environment_id": self.environment_id,
87
+ "metadata": dict(self.metadata),
88
+ }
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class SensorSample:
93
+ index: int
94
+ logical_time: str
95
+ values: Mapping[str, float]
96
+
97
+ def __post_init__(self) -> None:
98
+ if int(self.index) < 0:
99
+ raise ValueError("index must be non-negative")
100
+ object.__setattr__(self, "logical_time", _require_non_empty_string(self.logical_time, "logical_time"))
101
+ for key, value in self.values.items():
102
+ _require_non_empty_string(str(key), "value key")
103
+ _require_finite_float(value, f"values[{key}]")
104
+
105
+ def to_dict(self) -> dict[str, Any]:
106
+ return {
107
+ "schema_version": SENSOR_EVIDENCE_SCHEMA_VERSION,
108
+ "index": int(self.index),
109
+ "logical_time": self.logical_time,
110
+ "values": {key: float(self.values[key]) for key in sorted(self.values)},
111
+ }
112
+
113
+
114
+ @dataclass(frozen=True)
115
+ class SensorTrace:
116
+ trace_id: str
117
+ source: SensorSource
118
+ samples: tuple[SensorSample, ...]
119
+ encoding: str = SENSOR_TRACE_ENCODING
120
+ normalization_version: str = "sensor-evidence-bootstrap-v1"
121
+ metadata: Mapping[str, Any] = field(default_factory=dict)
122
+
123
+ def __post_init__(self) -> None:
124
+ object.__setattr__(self, "trace_id", _require_non_empty_string(self.trace_id, "trace_id"))
125
+ object.__setattr__(self, "encoding", _require_non_empty_string(self.encoding, "encoding"))
126
+ object.__setattr__(
127
+ self,
128
+ "normalization_version",
129
+ _require_non_empty_string(self.normalization_version, "normalization_version"),
130
+ )
131
+
132
+ def to_dict(self) -> dict[str, Any]:
133
+ return {
134
+ "schema_version": SENSOR_EVIDENCE_SCHEMA_VERSION,
135
+ "trace_id": self.trace_id,
136
+ "source": self.source.to_dict(),
137
+ "samples": [sample.to_dict() for sample in self.samples],
138
+ "sample_count": len(self.samples),
139
+ "encoding": self.encoding,
140
+ "normalization_version": self.normalization_version,
141
+ "metadata": dict(self.metadata),
142
+ }
143
+
144
+ def fingerprint(self) -> str:
145
+ return _sha256_text(_canonical_json(self.to_dict()))
146
+
147
+
148
+ @dataclass(frozen=True)
149
+ class ObservationEvent:
150
+ event_id: str
151
+ trace_id: str
152
+ sensor_id: str
153
+ event_type: str
154
+ logical_time: str
155
+ evidence_window: tuple[int, int]
156
+ input_fingerprint: str
157
+ output_fingerprint: str
158
+ confidence: float | None = None
159
+ uncertainty: float | None = None
160
+ processor_version: str = "sensor-evidence-bootstrap-v1"
161
+ metadata: Mapping[str, Any] = field(default_factory=dict)
162
+
163
+ def __post_init__(self) -> None:
164
+ object.__setattr__(self, "event_id", _require_non_empty_string(self.event_id, "event_id"))
165
+ object.__setattr__(self, "trace_id", _require_non_empty_string(self.trace_id, "trace_id"))
166
+ object.__setattr__(self, "sensor_id", _require_non_empty_string(self.sensor_id, "sensor_id"))
167
+ object.__setattr__(self, "event_type", _require_non_empty_string(self.event_type, "event_type"))
168
+ object.__setattr__(self, "logical_time", _require_non_empty_string(self.logical_time, "logical_time"))
169
+ object.__setattr__(
170
+ self,
171
+ "processor_version",
172
+ _require_non_empty_string(self.processor_version, "processor_version"),
173
+ )
174
+ object.__setattr__(self, "input_fingerprint", _require_non_empty_string(self.input_fingerprint, "input_fingerprint"))
175
+ object.__setattr__(self, "output_fingerprint", _require_non_empty_string(self.output_fingerprint, "output_fingerprint"))
176
+ if len(self.evidence_window) != 2:
177
+ raise ValueError("evidence_window must contain exactly two integers")
178
+ start, end = self.evidence_window
179
+ if int(start) < 0 or int(end) < 0:
180
+ raise ValueError("evidence_window values must be non-negative")
181
+ if int(start) > int(end):
182
+ raise ValueError("evidence_window start must be <= end")
183
+ if self.confidence is not None:
184
+ object.__setattr__(self, "confidence", _require_finite_float(self.confidence, "confidence"))
185
+ if self.uncertainty is not None:
186
+ object.__setattr__(self, "uncertainty", _require_finite_float(self.uncertainty, "uncertainty"))
187
+
188
+ def to_dict(self) -> dict[str, Any]:
189
+ return {
190
+ "schema_version": SENSOR_EVIDENCE_SCHEMA_VERSION,
191
+ "encoding": OBSERVATION_EVENT_ENCODING,
192
+ "event_id": self.event_id,
193
+ "trace_id": self.trace_id,
194
+ "sensor_id": self.sensor_id,
195
+ "event_type": self.event_type,
196
+ "logical_time": self.logical_time,
197
+ "evidence_window": [int(self.evidence_window[0]), int(self.evidence_window[1])],
198
+ "input_fingerprint": self.input_fingerprint,
199
+ "output_fingerprint": self.output_fingerprint,
200
+ "confidence": self.confidence,
201
+ "uncertainty": self.uncertainty,
202
+ "processor_version": self.processor_version,
203
+ "metadata": dict(self.metadata),
204
+ }
205
+
206
+ def fingerprint(self) -> str:
207
+ return _sha256_text(_canonical_json(self.to_dict()))
208
+
209
+
210
+ @dataclass(frozen=True)
211
+ class SensorFixtureManifest:
212
+ fixture_id: str
213
+ schema_version: str
214
+ trace_id: str
215
+ sensor_id: str
216
+ sample_count: int
217
+ value_keys: tuple[str, ...]
218
+ trace_fingerprint: str | None = None
219
+ observation_event_fingerprints: Mapping[str, str] = field(default_factory=dict)
220
+ notes: tuple[str, ...] = field(default_factory=tuple)
221
+
222
+ def __post_init__(self) -> None:
223
+ object.__setattr__(self, "fixture_id", _require_non_empty_string(self.fixture_id, "fixture_id"))
224
+ object.__setattr__(self, "schema_version", _require_non_empty_string(self.schema_version, "schema_version"))
225
+ object.__setattr__(self, "trace_id", _require_non_empty_string(self.trace_id, "trace_id"))
226
+ object.__setattr__(self, "sensor_id", _require_non_empty_string(self.sensor_id, "sensor_id"))
227
+ if int(self.sample_count) < 0:
228
+ raise ValueError("sample_count must be non-negative")
229
+ object.__setattr__(self, "value_keys", tuple(_require_non_empty_string(key, "value_key") for key in self.value_keys))
230
+ if self.trace_fingerprint is not None:
231
+ object.__setattr__(self, "trace_fingerprint", _require_non_empty_string(self.trace_fingerprint, "trace_fingerprint"))
232
+ normalized_events = {
233
+ _require_non_empty_string(key, "observation_event_fingerprint key"): _require_non_empty_string(
234
+ value,
235
+ "observation_event_fingerprint value",
236
+ )
237
+ for key, value in dict(self.observation_event_fingerprints).items()
238
+ }
239
+ object.__setattr__(self, "observation_event_fingerprints", normalized_events)
240
+ object.__setattr__(self, "notes", tuple(_require_non_empty_string(note, "note") for note in self.notes))
241
+
242
+ def to_dict(self) -> dict[str, Any]:
243
+ return {
244
+ "fixture_id": self.fixture_id,
245
+ "schema_version": self.schema_version,
246
+ "trace_id": self.trace_id,
247
+ "sensor_id": self.sensor_id,
248
+ "sample_count": int(self.sample_count),
249
+ "value_keys": list(self.value_keys),
250
+ "trace_fingerprint": self.trace_fingerprint,
251
+ "observation_event_fingerprints": dict(self.observation_event_fingerprints),
252
+ "notes": list(self.notes),
253
+ }
254
+
255
+
256
+ def load_sensor_fixture_manifest(path: str | Path) -> SensorFixtureManifest:
257
+ manifest_path = Path(path)
258
+ with manifest_path.open("r", encoding="utf-8") as handle:
259
+ data = json.load(handle)
260
+ return SensorFixtureManifest(
261
+ fixture_id=data["fixture_id"],
262
+ schema_version=data.get("schema_version", SENSOR_EVIDENCE_SCHEMA_VERSION),
263
+ trace_id=data["trace_id"],
264
+ sensor_id=data["sensor_id"],
265
+ sample_count=int(data["sample_count"]),
266
+ value_keys=tuple(data.get("value_keys", ())),
267
+ trace_fingerprint=data.get("trace_fingerprint"),
268
+ observation_event_fingerprints=dict(data.get("observation_event_fingerprints", {})),
269
+ notes=tuple(data.get("notes", ())),
270
+ )
271
+
272
+
273
+ def validate_sensor_trace_against_manifest(
274
+ trace: SensorTrace,
275
+ manifest: SensorFixtureManifest,
276
+ ) -> list[dict[str, Any]]:
277
+ warnings: list[dict[str, Any]] = []
278
+
279
+ if trace.trace_id != manifest.trace_id:
280
+ warnings.append(
281
+ {
282
+ "code": "trace_id_mismatch",
283
+ "message": "SensorTrace trace_id does not match manifest.",
284
+ "expected": manifest.trace_id,
285
+ "actual": trace.trace_id,
286
+ }
287
+ )
288
+
289
+ if trace.source.sensor_id != manifest.sensor_id:
290
+ warnings.append(
291
+ {
292
+ "code": "sensor_id_mismatch",
293
+ "message": "SensorTrace sensor_id does not match manifest.",
294
+ "expected": manifest.sensor_id,
295
+ "actual": trace.source.sensor_id,
296
+ }
297
+ )
298
+
299
+ if len(trace.samples) != manifest.sample_count:
300
+ warnings.append(
301
+ {
302
+ "code": "sample_count_mismatch",
303
+ "message": "SensorTrace sample count does not match manifest.",
304
+ "expected": manifest.sample_count,
305
+ "actual": len(trace.samples),
306
+ }
307
+ )
308
+
309
+ actual_value_keys = tuple(
310
+ sorted(
311
+ {
312
+ key
313
+ for sample in trace.samples
314
+ for key in sample.values
315
+ if isinstance(key, str) and key.strip()
316
+ }
317
+ )
318
+ )
319
+ if tuple(manifest.value_keys) != actual_value_keys:
320
+ warnings.append(
321
+ {
322
+ "code": "value_key_mismatch",
323
+ "message": "SensorTrace value keys do not match manifest.",
324
+ "expected": list(manifest.value_keys),
325
+ "actual": list(actual_value_keys),
326
+ }
327
+ )
328
+
329
+ if manifest.trace_fingerprint is not None:
330
+ actual_trace_fingerprint = trace.fingerprint()
331
+ if actual_trace_fingerprint != manifest.trace_fingerprint:
332
+ warnings.append(
333
+ {
334
+ "code": "trace_fingerprint_mismatch",
335
+ "message": "SensorTrace fingerprint does not match manifest.",
336
+ "expected": manifest.trace_fingerprint,
337
+ "actual": actual_trace_fingerprint,
338
+ }
339
+ )
340
+
341
+ return warnings
342
+
343
+
344
+ def observation_event_to_explainability_artifacts(
345
+ event: ObservationEvent,
346
+ *,
347
+ fact_id: str | None = None,
348
+ node_id: str | None = None,
349
+ ) -> dict[str, dict[str, Any]]:
350
+ resolved_fact_id = fact_id or f"fact:{event.event_id}"
351
+ resolved_node_id = node_id or f"node:{event.event_id}"
352
+
353
+ event_log = {
354
+ "events": {
355
+ event.event_id: {
356
+ "schema_version": SENSOR_EVIDENCE_SCHEMA_VERSION,
357
+ "encoding": OBSERVATION_EVENT_ENCODING,
358
+ "event_id": event.event_id,
359
+ "event_type": event.event_type,
360
+ "trace_id": event.trace_id,
361
+ "sensor_id": event.sensor_id,
362
+ "input_fingerprint": event.input_fingerprint,
363
+ "output_fingerprint": event.output_fingerprint,
364
+ }
365
+ }
366
+ }
367
+
368
+ knowledge_base = {
369
+ "facts": {
370
+ resolved_fact_id: {
371
+ "schema_version": SENSOR_EVIDENCE_SCHEMA_VERSION,
372
+ "fact_id": resolved_fact_id,
373
+ "metadata": {
374
+ "source_event_ids": [event.event_id],
375
+ "trace_id": event.trace_id,
376
+ "sensor_id": event.sensor_id,
377
+ "event_type": event.event_type,
378
+ },
379
+ }
380
+ }
381
+ }
382
+
383
+ execution_graph = {
384
+ "nodes": {
385
+ resolved_node_id: {
386
+ "schema_version": SENSOR_EVIDENCE_SCHEMA_VERSION,
387
+ "node_id": resolved_node_id,
388
+ "node_type": "ObservationEmitted",
389
+ "event_ids": [event.event_id],
390
+ "fact_ids": [resolved_fact_id],
391
+ "trace_id": event.trace_id,
392
+ "sensor_id": event.sensor_id,
393
+ }
394
+ },
395
+ "edges": [],
396
+ "metadata": {
397
+ "sensor_event_id": event.event_id,
398
+ "sensor_trace_id": event.trace_id,
399
+ "sensor_id": event.sensor_id,
400
+ },
401
+ }
402
+
403
+ return {
404
+ "event_log": event_log,
405
+ "knowledge_base": knowledge_base,
406
+ "execution_graph": execution_graph,
407
+ }
408
+
409
+
410
+ def load_sensor_trace_csv(
411
+ path: str | Path,
412
+ *,
413
+ trace_id: str,
414
+ source: SensorSource,
415
+ ) -> SensorTrace:
416
+ """Load a deterministic SensorTrace from CSV.
417
+
418
+ Expected CSV columns:
419
+ - index
420
+ - logical_time
421
+ - one or more numeric value columns
422
+
423
+ This loader is for offline fixtures only.
424
+ """
425
+
426
+ csv_path = Path(path)
427
+ samples: list[SensorSample] = []
428
+
429
+ with csv_path.open("r", encoding="utf-8", newline="") as handle:
430
+ reader = csv.DictReader(handle)
431
+ if reader.fieldnames is None:
432
+ raise ValueError("CSV fixture must include a header")
433
+
434
+ required = {"index", "logical_time"}
435
+ missing = required.difference(reader.fieldnames)
436
+ if missing:
437
+ raise ValueError(f"CSV fixture missing required columns: {sorted(missing)}")
438
+
439
+ value_columns = [name for name in reader.fieldnames if name not in required]
440
+ if not value_columns:
441
+ raise ValueError("CSV fixture must include at least one value column")
442
+
443
+ for row in reader:
444
+ values = {column: _require_finite_float(row[column], f"values[{column}]") for column in value_columns}
445
+ samples.append(
446
+ SensorSample(
447
+ index=int(row["index"]),
448
+ logical_time=row["logical_time"],
449
+ values=values,
450
+ )
451
+ )
452
+
453
+ samples = sorted(samples, key=lambda sample: sample.index)
454
+
455
+ return SensorTrace(
456
+ trace_id=trace_id,
457
+ source=source,
458
+ samples=tuple(samples),
459
+ metadata={
460
+ "source_path": str(csv_path),
461
+ "sample_count": len(samples),
462
+ "value_keys": list(sorted(samples[0].values)) if samples else [],
463
+ },
464
+ )
465
+
466
+
467
+ def derive_threshold_observation_event(
468
+ trace: SensorTrace,
469
+ *,
470
+ event_id: str,
471
+ event_type: str,
472
+ value_key: str,
473
+ threshold: float,
474
+ processor_version: str = "sensor-evidence-bootstrap-v1",
475
+ ) -> ObservationEvent:
476
+ """Derive a deterministic ObservationEvent from a simple threshold rule.
477
+
478
+ This is intentionally simple and fixture-oriented. It is not a general
479
+ inference engine and does not imply causal reasoning.
480
+ """
481
+
482
+ normalized_value_key = _require_non_empty_string(value_key, "value_key")
483
+ threshold_value = _require_finite_float(threshold, "threshold")
484
+
485
+ matching = [
486
+ sample
487
+ for sample in trace.samples
488
+ if normalized_value_key in sample.values and sample.values[normalized_value_key] >= threshold_value
489
+ ]
490
+
491
+ if not matching:
492
+ evidence_window = (0, 0)
493
+ logical_time = trace.samples[0].logical_time if trace.samples else "t0"
494
+ confidence = 0.0
495
+ else:
496
+ evidence_window = (matching[0].index, matching[-1].index)
497
+ logical_time = matching[0].logical_time
498
+ confidence = min(1.0, len(matching) / max(1, len(trace.samples)))
499
+
500
+ input_fingerprint = trace.fingerprint()
501
+ event_payload = {
502
+ "event_id": event_id,
503
+ "trace_id": trace.trace_id,
504
+ "sensor_id": trace.source.sensor_id,
505
+ "event_type": event_type,
506
+ "logical_time": logical_time,
507
+ "evidence_window": list(evidence_window),
508
+ "value_key": normalized_value_key,
509
+ "threshold": threshold_value,
510
+ "processor_version": processor_version,
511
+ }
512
+ output_fingerprint = _sha256_text(_canonical_json(event_payload))
513
+
514
+ return ObservationEvent(
515
+ event_id=event_id,
516
+ trace_id=trace.trace_id,
517
+ sensor_id=trace.source.sensor_id,
518
+ event_type=event_type,
519
+ logical_time=logical_time,
520
+ evidence_window=evidence_window,
521
+ input_fingerprint=input_fingerprint,
522
+ output_fingerprint=output_fingerprint,
523
+ confidence=confidence,
524
+ uncertainty=None if confidence == 1.0 else 1.0 - confidence,
525
+ processor_version=processor_version,
526
+ metadata={
527
+ "value_key": normalized_value_key,
528
+ "threshold": threshold_value,
529
+ "matching_sample_count": len(matching),
530
+ },
531
+ )
532
+
533
+
534
+ __all__ = [
535
+ "SENSOR_EVIDENCE_SCHEMA_VERSION",
536
+ "SENSOR_TRACE_ENCODING",
537
+ "OBSERVATION_EVENT_ENCODING",
538
+ "SensorSource",
539
+ "SensorSample",
540
+ "SensorTrace",
541
+ "SensorFixtureManifest",
542
+ "ObservationEvent",
543
+ "load_sensor_fixture_manifest",
544
+ "validate_sensor_trace_against_manifest",
545
+ "observation_event_to_explainability_artifacts",
546
+ "load_sensor_trace_csv",
547
+ "derive_threshold_observation_event",
548
+ ]