arbiter-engine 0.1.0__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 (63) hide show
  1. arbiter_engine/__init__.py +31 -0
  2. arbiter_engine/api.py +473 -0
  3. arbiter_engine/axiom_thresholds.py +118 -0
  4. arbiter_engine/envelope.py +270 -0
  5. arbiter_engine/examples/water_tank.yaml +112 -0
  6. arbiter_engine/fire_frequency.py +252 -0
  7. arbiter_engine/history/__init__.py +12 -0
  8. arbiter_engine/history/observation.py +596 -0
  9. arbiter_engine/history/observation_production.py +335 -0
  10. arbiter_engine/history/observation_source_wiring.py +120 -0
  11. arbiter_engine/history/readiness.py +288 -0
  12. arbiter_engine/interfaces.py +1353 -0
  13. arbiter_engine/mcp/__init__.py +1 -0
  14. arbiter_engine/mcp/server.py +221 -0
  15. arbiter_engine/ontology/__init__.py +18 -0
  16. arbiter_engine/ontology/axiom_verdicts_production.py +332 -0
  17. arbiter_engine/ontology/axioms/__init__.py +26 -0
  18. arbiter_engine/ontology/axioms/boundedness.py +340 -0
  19. arbiter_engine/ontology/axioms/connectivity.py +396 -0
  20. arbiter_engine/ontology/axioms/conservation.py +266 -0
  21. arbiter_engine/ontology/axioms/consistency.py +422 -0
  22. arbiter_engine/ontology/axioms/extensions.py +106 -0
  23. arbiter_engine/ontology/axioms/homeostasis.py +783 -0
  24. arbiter_engine/ontology/axioms/monotonicity.py +444 -0
  25. arbiter_engine/ontology/axioms/responsiveness.py +752 -0
  26. arbiter_engine/ontology/axioms/roles.py +207 -0
  27. arbiter_engine/ontology/axioms/stability.py +327 -0
  28. arbiter_engine/ontology/domain_loader.py +485 -0
  29. arbiter_engine/ontology/loader.py +802 -0
  30. arbiter_engine/ontology/reasoner.py +1069 -0
  31. arbiter_engine/propagation/__init__.py +1 -0
  32. arbiter_engine/propagation/impact_estimator.py +305 -0
  33. arbiter_engine/propagation/lp_confidence.py +123 -0
  34. arbiter_engine/propagation/mcts_root_cause.py +266 -0
  35. arbiter_engine/propagation/root_cause.py +559 -0
  36. arbiter_engine/propagation/weight_learner.py +216 -0
  37. arbiter_engine/rca/__init__.py +4 -0
  38. arbiter_engine/rca/greedy_set_cover.py +291 -0
  39. arbiter_engine/residual/__init__.py +1 -0
  40. arbiter_engine/residual/predict_vs_mirror.py +542 -0
  41. arbiter_engine/temporal/__init__.py +1 -0
  42. arbiter_engine/temporal/temporal_edge.py +584 -0
  43. arbiter_engine/temporal/trend_projection.py +430 -0
  44. arbiter_engine/twin/__init__.py +23 -0
  45. arbiter_engine/twin/action_clears_problem.py +217 -0
  46. arbiter_engine/twin/builder.py +479 -0
  47. arbiter_engine/twin/gap.py +132 -0
  48. arbiter_engine/twin/hypothesis_generator.py +530 -0
  49. arbiter_engine/twin/hypothesis_production.py +236 -0
  50. arbiter_engine/twin/kernel_pipeline_executor.py +571 -0
  51. arbiter_engine/twin/monte_carlo_predictor.py +1001 -0
  52. arbiter_engine/twin/optimization_production.py +233 -0
  53. arbiter_engine/twin/pipeline_production.py +176 -0
  54. arbiter_engine/twin/topology.py +450 -0
  55. arbiter_engine/twin/topology_optimizer.py +412 -0
  56. arbiter_engine/twin/traverser.py +1260 -0
  57. arbiter_engine/twin/traverser_production.py +283 -0
  58. arbiter_engine/types.py +427 -0
  59. arbiter_engine-0.1.0.dist-info/METADATA +278 -0
  60. arbiter_engine-0.1.0.dist-info/RECORD +63 -0
  61. arbiter_engine-0.1.0.dist-info/WHEEL +4 -0
  62. arbiter_engine-0.1.0.dist-info/licenses/LICENSE +202 -0
  63. arbiter_engine-0.1.0.dist-info/licenses/NOTICE +18 -0
@@ -0,0 +1,288 @@
1
+ """
2
+ Axiom Readiness Tracking.
3
+
4
+ Tracks which axioms are ready to be checked for each entity
5
+ based on available observation history.
6
+ """
7
+
8
+ import logging
9
+ from datetime import datetime
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from ..interfaces import Entity, ObservationHistory
13
+ from ..types import Axiom, AxiomReadiness, AXIOM_MINIMUMS # noqa: F401
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ # Minimum observations required for each axiom.
19
+ #
20
+ # the canonical table moved to ``arbiter_engine/types.py``, beside the
21
+ # enum it keys on. Two of its eight values had drifted from the copies in
22
+ # ``ontology/reasoner.py``; one definition removes the class of bug.
23
+ # Re-exported above so existing ``...readiness import AXIOM_MINIMUMS``
24
+ # imports keep working.
25
+
26
+
27
+ class AxiomReadinessTracker:
28
+ """
29
+ Track and report axiom readiness across all entities.
30
+
31
+ Axiom readiness depends on:
32
+ - Number of historical observations
33
+ - Time span of observations
34
+ - Quality of data (no gaps, consistent sampling)
35
+ """
36
+
37
+ def __init__(self, custom_minimums: Optional[Dict[Axiom, int]] = None,
38
+ sample_interval_seconds: float = 1.0):
39
+ self.minimums = AXIOM_MINIMUMS.copy()
40
+ if custom_minimums:
41
+ self.minimums.update(custom_minimums)
42
+
43
+ # Adjust minimums based on observation sample interval.
44
+ # Slower-sampling domains need fewer observations (each carries more info).
45
+ if sample_interval_seconds > 1.0:
46
+ self.minimums = {
47
+ axiom: self._get_adjusted_minimum(base, sample_interval_seconds)
48
+ for axiom, base in self.minimums.items()
49
+ }
50
+
51
+ @staticmethod
52
+ def _get_adjusted_minimum(base: int, sample_interval: float) -> int:
53
+ """Adjust axiom minimum based on sample interval.
54
+
55
+ Fast sampling (<=1s): use base minimums (K8s standard).
56
+ Moderate (1-10s): halve minimums (each obs is more informative).
57
+ Slow (>10s): third of minimums (hardware, network domains).
58
+ """
59
+ if sample_interval <= 1.0:
60
+ return base
61
+ elif sample_interval <= 10.0:
62
+ return max(3, base // 2)
63
+ else:
64
+ return max(2, base // 3)
65
+
66
+ def get_entity_readiness(
67
+ self,
68
+ entity: Entity,
69
+ history: ObservationHistory,
70
+ properties: Optional[List[str]] = None
71
+ ) -> Dict[Axiom, AxiomReadiness]:
72
+ """
73
+ Get axiom readiness for a single entity.
74
+
75
+ Uses per-property observation timestamps to infer actual
76
+ sample interval instead of relying on the global default. Mixed-rate
77
+ entities (e.g., temperature at 1s, firmware version at 60s) get
78
+ per-property adjusted minimums.
79
+
80
+ Args:
81
+ entity: Entity to check
82
+ history: Observation history
83
+ properties: Properties to check (all if None)
84
+
85
+ Returns:
86
+ Dict mapping axiom to readiness status
87
+ """
88
+ readiness = {}
89
+
90
+ # Get observation counts for entity properties
91
+ max_counts: Dict[Axiom, int] = {axiom: 0 for axiom in Axiom}
92
+
93
+ # Properties to check
94
+ if properties is None:
95
+ properties = self._extract_property_names(entity)
96
+
97
+ # Estimate per-property sample interval from observation history.
98
+ # Use the interval to apply property-specific adjusted minimums for
99
+ # mixed-rate entities where different properties sample at different rates.
100
+ _prop_intervals: Dict[str, float] = {}
101
+ if hasattr(history, 'get_observation_timestamps'):
102
+ for prop in (properties or []):
103
+ try:
104
+ timestamps = history.get_observation_timestamps(entity.id, prop, limit=10)
105
+ if len(timestamps) >= 2:
106
+ deltas = [
107
+ (timestamps[i] - timestamps[i - 1]).total_seconds()
108
+ for i in range(1, len(timestamps))
109
+ if (timestamps[i] - timestamps[i - 1]).total_seconds() > 0
110
+ ]
111
+ if deltas:
112
+ _prop_intervals[prop] = sum(deltas) / len(deltas)
113
+ except Exception:
114
+ pass
115
+
116
+ for prop in properties:
117
+ count = history.get_observation_count(entity.id, prop)
118
+
119
+ # Assign count to relevant axioms based on property type
120
+ value = entity.get_property(prop)
121
+
122
+ if isinstance(value, (int, float)):
123
+ # Numeric: relevant for BOUNDEDNESS, HOMEOSTASIS, and — added
124
+ # after the KeyError fix exposed it — CONSERVATION and
125
+ # MONOTONICITY. Both are numeric-series axioms, but neither was
126
+ # ever assigned a count here, so both sat at 0 and reported
127
+ # never-ready for every entity. That silently and permanently
128
+ # downweighted every CONSERVATION/MONOTONICITY problem the
129
+ # LayeredDetector raised, via _adjust_confidence. Adding the
130
+ # minimums un-crashed the pass; it did not make the table true.
131
+ max_counts[Axiom.BOUNDEDNESS] = max(max_counts[Axiom.BOUNDEDNESS], count)
132
+ max_counts[Axiom.HOMEOSTASIS] = max(max_counts[Axiom.HOMEOSTASIS], count)
133
+ max_counts[Axiom.CONSERVATION] = max(max_counts[Axiom.CONSERVATION], count)
134
+ max_counts[Axiom.MONOTONICITY] = max(max_counts[Axiom.MONOTONICITY], count)
135
+ elif isinstance(value, str):
136
+ # State: relevant for STABILITY, CONSISTENCY
137
+ max_counts[Axiom.STABILITY] = max(max_counts[Axiom.STABILITY], count)
138
+ max_counts[Axiom.CONSISTENCY] = max(max_counts[Axiom.CONSISTENCY], count)
139
+
140
+ # All properties count for RESPONSIVENESS
141
+ max_counts[Axiom.RESPONSIVENESS] = max(max_counts[Axiom.RESPONSIVENESS], count)
142
+
143
+ # CONNECTIVITY doesn't need history
144
+ max_counts[Axiom.CONNECTIVITY] = 1
145
+
146
+ # Build readiness report
147
+ for axiom in Axiom:
148
+ # Degrade-with-surfacing rather than KeyError: a future axiom added
149
+ # to the enum without a minimums entry should narrow readiness for
150
+ # that one axiom, not crash the entire detection pass.
151
+ if axiom not in self.minimums:
152
+ logger.warning(
153
+ "AxiomReadinessTracker: no minimum configured for %s — "
154
+ "treating it as never-ready. Add it to AXIOM_MINIMUMS.",
155
+ axiom,
156
+ )
157
+ required = self.minimums.get(axiom, 2 ** 31)
158
+ count = max_counts[axiom]
159
+ is_ready = count >= required
160
+ ratio = min(1.0, count / required) if required > 0 else 1.0
161
+
162
+ readiness[axiom] = AxiomReadiness(
163
+ axiom=axiom,
164
+ entity_id=entity.id,
165
+ entity_type=entity.type,
166
+ observations_count=count,
167
+ required_count=required,
168
+ is_ready=is_ready,
169
+ readiness_ratio=ratio,
170
+ )
171
+
172
+ return readiness
173
+
174
+ def _extract_property_names(self, entity: Entity) -> List[str]:
175
+ """Extract all property names from entity."""
176
+ properties = []
177
+
178
+ def extract(props: dict, prefix: str = ''):
179
+ for key, value in props.items():
180
+ path = f"{prefix}.{key}" if prefix else key
181
+ if isinstance(value, dict):
182
+ extract(value, path)
183
+ else:
184
+ properties.append(path)
185
+
186
+ extract(entity.properties)
187
+ return properties
188
+
189
+ def get_readiness_report(
190
+ self,
191
+ entities: List[Entity],
192
+ history: ObservationHistory
193
+ ) -> Dict:
194
+ """
195
+ Generate system-wide readiness report.
196
+
197
+ Returns:
198
+ Dict with overall readiness statistics
199
+ """
200
+ total_by_axiom = {axiom: {'ready': 0, 'not_ready': 0} for axiom in Axiom}
201
+ entity_reports = {}
202
+
203
+ for entity in entities:
204
+ readiness = self.get_entity_readiness(entity, history)
205
+ entity_reports[entity.id] = readiness
206
+
207
+ for axiom, status in readiness.items():
208
+ if status.is_ready:
209
+ total_by_axiom[axiom]['ready'] += 1
210
+ else:
211
+ total_by_axiom[axiom]['not_ready'] += 1
212
+
213
+ # Calculate percentages
214
+ summary = {}
215
+ for axiom in Axiom:
216
+ ready = total_by_axiom[axiom]['ready']
217
+ not_ready = total_by_axiom[axiom]['not_ready']
218
+ total = ready + not_ready
219
+
220
+ summary[axiom.value] = {
221
+ 'ready_count': ready,
222
+ 'not_ready_count': not_ready,
223
+ 'ready_percentage': (ready / total * 100) if total > 0 else 0,
224
+ #.get() for the same reason as the per-entity loop: a 9th enum
225
+ # member added without a minimums entry should narrow the report,
226
+ # not KeyError the summary. The per-entity path was guarded and
227
+ # this one was missed — the guard is only as good as its coverage.
228
+ 'minimum_required': self.minimums.get(axiom),
229
+ }
230
+
231
+ return {
232
+ 'entity_count': len(entities),
233
+ 'axiom_summary': summary,
234
+ 'timestamp': datetime.utcnow().isoformat(),
235
+ }
236
+
237
+ def get_warmup_status(
238
+ self,
239
+ entities: List[Entity],
240
+ history: ObservationHistory
241
+ ) -> Dict[str, Dict[str, Any]]:
242
+ """Get observable warmup progress per entity per axiom.
243
+
244
+ Returns a dict keyed by entity_id with per-axiom warmup progress.
245
+ Operators can distinguish "healthy" from "still warming up".
246
+ """
247
+ status = {}
248
+ for entity in entities:
249
+ readiness = self.get_entity_readiness(entity, history)
250
+ entity_status = {}
251
+ for axiom, r in readiness.items():
252
+ entity_status[axiom.value] = {
253
+ 'is_ready': r.is_ready,
254
+ 'observations': r.observations_count,
255
+ 'required': r.required_count,
256
+ 'progress_pct': min(100, int(
257
+ r.observations_count / r.required_count * 100
258
+ )) if r.required_count > 0 else 100,
259
+ }
260
+ status[entity.id] = entity_status
261
+ return status
262
+
263
+ def adjust_detection_confidence(
264
+ self,
265
+ problem: 'Problem',
266
+ readiness: AxiomReadiness
267
+ ) -> 'Problem':
268
+ """
269
+ Adjust problem confidence based on axiom readiness.
270
+
271
+ Problems detected with insufficient history get lower confidence.
272
+ """
273
+ if readiness.is_ready:
274
+ return problem
275
+
276
+ # Scale confidence by readiness ratio
277
+ adjusted_confidence = problem.confidence * readiness.readiness_ratio
278
+
279
+ # Update evidence
280
+ problem.evidence['axiom_readiness'] = {
281
+ 'observations': readiness.observations_count,
282
+ 'required': readiness.required_count,
283
+ 'ratio': readiness.readiness_ratio,
284
+ }
285
+ problem.confidence = adjusted_confidence
286
+ problem.metadata['confidence_adjusted'] = True
287
+
288
+ return problem