core-runtime-engine 11.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- core_runtime/__init__.py +10 -0
- core_runtime/__version__.py +17 -0
- core_runtime/cli/__init__.py +7 -0
- core_runtime/cli/__main__.py +22 -0
- core_runtime/cli/bump_version.py +176 -0
- core_runtime/cli/contract_preflight.py +69 -0
- core_runtime/cli/create_domain.py +66 -0
- core_runtime/cli/doctor.py +44 -0
- core_runtime/cli/inventory.py +85 -0
- core_runtime/cli/lint.py +8 -0
- core_runtime/cli/main.py +579 -0
- core_runtime/cli/release_check.py +65 -0
- core_runtime/cli/repair_artifact_paths.py +48 -0
- core_runtime/cli/sync_template.py +67 -0
- core_runtime/cli/validate.py +73 -0
- core_runtime/core/__init__.py +34 -0
- core_runtime/core/audit_event.py +760 -0
- core_runtime/core/audit_trail_index.py +100 -0
- core_runtime/core/canonicalization.py +55 -0
- core_runtime/core/contract_evaluator.py +945 -0
- core_runtime/core/contract_executability.py +307 -0
- core_runtime/core/contract_loader.py +57 -0
- core_runtime/core/contract_probes.py +630 -0
- core_runtime/core/contract_program.py +131 -0
- core_runtime/core/contract_program_registry.py +104 -0
- core_runtime/core/contract_program_v2.py +126 -0
- core_runtime/core/dsk_v3.py +142 -0
- core_runtime/core/explainability.py +2115 -0
- core_runtime/core/numeric_normalization.py +120 -0
- core_runtime/core/rule_anchor.py +1388 -0
- core_runtime/core/schema_fingerprint.py +69 -0
- core_runtime/core/sensor_evidence.py +548 -0
- core_runtime/data/contracts/CoreAnchor.sol +109 -0
- core_runtime/data/contracts/CoreRuleAnchor.abi.json +111 -0
- core_runtime/data/contracts/CoreRuleAnchor.bin +1 -0
- core_runtime/data/contracts/CoreRuleAnchor.build.json +20 -0
- core_runtime/data/contracts/CoreRuleAnchor.runtime.bin +1 -0
- core_runtime/data/contracts/CoreRuleAnchor.sol +74 -0
- core_runtime/data/package_data_manifest.v1.json +173 -0
- core_runtime/data/schemas/core/causal_trace.v1.json +141 -0
- core_runtime/data/schemas/core/context_gate.v1.json +38 -0
- core_runtime/data/schemas/core/context_threshold.v1.json +46 -0
- core_runtime/data/schemas/core/contract_program.v1.json +186 -0
- core_runtime/data/schemas/core/contract_program.v2.json +187 -0
- core_runtime/data/schemas/core/control_decision.v1.json +114 -0
- core_runtime/data/schemas/core/dsk.v3.json +105 -0
- core_runtime/data/schemas/core/effect_result.v1.json +39 -0
- core_runtime/data/schemas/core/entropy_signal.v1.json +108 -0
- core_runtime/data/schemas/core/execution_receipt.v1.json +93 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v1.json +87 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v2.json +72 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v3.json +38 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v4.json +72 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v5.json +38 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v6.json +116 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v7.json +37 -0
- core_runtime/data/schemas/core/frozen_release_manifest.v8.json +114 -0
- core_runtime/data/schemas/core/frozen_rule_set.v1.json +282 -0
- core_runtime/data/schemas/core/memory_artifact.v1.json +120 -0
- core_runtime/data/schemas/core/memory_generation_result.v1.json +37 -0
- core_runtime/data/schemas/core/operational_learning_event.v1.json +63 -0
- core_runtime/data/schemas/core/pattern_candidate.v1.json +114 -0
- core_runtime/data/schemas/core/physical_safety_assurance_case.v1.json +676 -0
- core_runtime/data/schemas/core/policy_lifecycle.v1.json +99 -0
- core_runtime/data/schemas/core/retention_manifest.v1.json +53 -0
- core_runtime/data/schemas/core/reversibility_policy.v1.json +107 -0
- core_runtime/data/schemas/core/rule_anchor_batch.v1.json +93 -0
- core_runtime/data/schemas/core/rule_anchor_chain_evidence.v1.json +56 -0
- core_runtime/data/schemas/core/rule_approval.v1.json +49 -0
- core_runtime/data/schemas/core/rule_approval_request.v1.json +42 -0
- core_runtime/data/schemas/core/state_transition.v1.json +115 -0
- core_runtime/data/schemas/core/task_closeout.v1.json +47 -0
- core_runtime/data/schemas/core/template_promotion_candidate.v1.json +83 -0
- core_runtime/data/schemas/core/unsigned_rule_anchor_deployment.v1.json +106 -0
- core_runtime/data/schemas/core/unsigned_rule_anchor_transaction.v1.json +116 -0
- core_runtime/tooling/__init__.py +48 -0
- core_runtime/tooling/bump_version.py +900 -0
- core_runtime/tooling/contract_preflight.py +293 -0
- core_runtime/tooling/create_domain.py +254 -0
- core_runtime/tooling/diagnostics.py +129 -0
- core_runtime/tooling/doctor.py +482 -0
- core_runtime/tooling/file_inventory.py +182 -0
- core_runtime/tooling/json_checks.py +100 -0
- core_runtime/tooling/release_check.py +1017 -0
- core_runtime/tooling/repair_artifact_paths.py +458 -0
- core_runtime/tooling/report_writer.py +176 -0
- core_runtime/tooling/repository_inventory.py +399 -0
- core_runtime/tooling/safety_checks.py +172 -0
- core_runtime/tooling/sync_template.py +303 -0
- core_runtime/tooling/validation.py +507 -0
- core_runtime/tooling/version_inventory.py +256 -0
- core_runtime_engine-11.5.1.dist-info/METADATA +35 -0
- core_runtime_engine-11.5.1.dist-info/RECORD +96 -0
- core_runtime_engine-11.5.1.dist-info/WHEEL +5 -0
- core_runtime_engine-11.5.1.dist-info/entry_points.txt +2 -0
- core_runtime_engine-11.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,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
|
+
]
|