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,340 @@
1
+ """
2
+ BOUNDEDNESS Axiom Checker.
3
+
4
+ BOUNDEDNESS: System operates within limits.
5
+
6
+ Detects:
7
+ - Threshold violations (immediate)
8
+ - Trending toward limits (predictive)
9
+ - Capacity exhaustion
10
+
11
+ Mathematical formula for trend:
12
+ slope = Σ(tᵢ - t̄)(Pᵢ - P̄) / Σ(tᵢ - t̄)²
13
+ trend_severity = |slope| × R² / Var(P)
14
+
15
+ Parameters:
16
+ - trend_min_r2 = 0.7: Minimum R² for significant trend
17
+ - exhaustion_warning = 0.8: 80% of limit
18
+ - exhaustion_critical = 0.95: 95% of limit
19
+ """
20
+
21
+ import logging
22
+ from datetime import timedelta
23
+ from typing import Any, List, Optional
24
+
25
+ from ...interfaces import (
26
+ Entity,
27
+ Problem,
28
+ RelationshipGraph,
29
+ ObservationHistory,
30
+ IndicatorSpec,
31
+ calculate_trend,
32
+ apply_property_confidence,
33
+ CheckOutcome,
34
+ )
35
+ from ...types import (
36
+ Axiom, Severity, AxiomParameters, DetectionLayer, NotEvaluatedReason,
37
+ )
38
+ # — call resolve_axiom_threshold at calibration-relevant
39
+ # read-sites so per-sample entity-property overrides win over
40
+ # global AxiomParameters fallback during v2 perturbation runs. Non-
41
+ # calibration reads (trend_min_r2, time_to_critical_hours) stay as
42
+ # global params — those are operator-tuning knobs, not per-axiom-per-
43
+ # entity calibration thresholds.
44
+ from ...axiom_thresholds import (
45
+ resolve_axiom_threshold,
46
+ )
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+
51
+ class BoundednessChecker:
52
+ """
53
+ Check BOUNDEDNESS axiom for entities.
54
+
55
+ BOUNDEDNESS can work from cold start (threshold checks)
56
+ but requires history for trend detection.
57
+ Minimum observations for trend: 5
58
+
59
+ : optional ``overlay: RuntimeYAMLOverlay`` lets the
60
+ checker consult approved threshold mutations (sub-
61
+ decision (B) per-domain overlay file). When provided, the checker
62
+ resolves the effective warning/critical thresholds via
63
+ ``overlay.get_threshold(entity_domain, indicator.name,
64
+ threshold_type, default=indicator.<type>_threshold)``. When None
65
+ or no override is set, behavior is unchanged from previously.
66
+ The entity's ``metadata['domain_id']`` is consulted for the
67
+ domain key; if absent, the overlay is bypassed for that entity.
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ params: Optional[AxiomParameters] = None,
73
+ overlay: Optional[Any] = None,
74
+ ):
75
+ self.params = params or AxiomParameters()
76
+ self.overlay = overlay
77
+
78
+ def _effective_threshold(
79
+ self,
80
+ entity: Entity,
81
+ indicator: IndicatorSpec,
82
+ threshold_type: str,
83
+ default: Optional[float],
84
+ ) -> Optional[float]:
85
+ """return overlay-resolved threshold or default.
86
+
87
+ Falls back to ``default`` if (a) no overlay is wired,
88
+ (b) entity has no ``metadata['domain_id']``, or (c) the
89
+ overlay has no override for this (domain, indicator,
90
+ threshold_type) triple.
91
+ """
92
+ if self.overlay is None:
93
+ return default
94
+ domain = entity.metadata.get('domain_id') if entity.metadata else None
95
+ if not domain:
96
+ return default
97
+ return self.overlay.get_threshold(
98
+ domain, indicator.name, threshold_type, default
99
+ )
100
+
101
+ def check(
102
+ self,
103
+ entity: Entity,
104
+ indicator: IndicatorSpec,
105
+ graph: RelationshipGraph,
106
+ history: ObservationHistory
107
+ ) -> List[Problem]:
108
+ """
109
+ Check BOUNDEDNESS for an entity/indicator.
110
+
111
+ 1. Check immediate threshold violations
112
+ 2. Check trend toward limits
113
+ 3. Estimate time to critical
114
+ """
115
+ problems = []
116
+
117
+ # Only check numeric indicators
118
+ if indicator.indicator_type.value != 'numeric':
119
+ return problems
120
+
121
+ # Get current value
122
+ current = entity.get_property(indicator.property_name)
123
+ if current is None:
124
+ return problems
125
+
126
+ try:
127
+ current = float(current)
128
+ except (TypeError, ValueError):
129
+ return problems
130
+
131
+ #: resolve effective thresholds through the optional
132
+ # RuntimeYAMLOverlay. When no overlay is wired or no override
133
+ # exists, the calls return the original IndicatorSpec values.
134
+ critical_threshold = self._effective_threshold(
135
+ entity, indicator, 'critical', indicator.critical_threshold
136
+ )
137
+ warning_threshold = self._effective_threshold(
138
+ entity, indicator, 'warning', indicator.warning_threshold
139
+ )
140
+
141
+ # 1. Check immediate threshold violations
142
+ if critical_threshold is not None:
143
+ if current >= critical_threshold:
144
+ problems.append(Problem.from_entity(
145
+ entity=entity,
146
+ problem_type=f'threshold_exceeded:{indicator.name}',
147
+ severity=Severity.CRITICAL,
148
+ reason=f"{indicator.name} exceeds critical threshold",
149
+ axiom=Axiom.BOUNDEDNESS,
150
+ source_layer=DetectionLayer.ONTOLOGY,
151
+ evidence={
152
+ 'indicator': indicator.name,
153
+ 'value': current,
154
+ 'threshold': critical_threshold,
155
+ 'threshold_type': 'critical',
156
+ },
157
+ confidence=1.0,
158
+ ))
159
+ # early-exit short-circuits the warning_threshold +
160
+ # trend checks (don't double-fire on same threshold breach)
161
+ # but must STILL flow through ``apply_property_confidence``
162
+ # so stale-property modulation applies — same return-shape
163
+ # as the warning + trend + end-of-function paths. Pre-fix
164
+ # this branch returned ``problems`` raw, leaving CRITICAL
165
+ # severity un-modulated for stale properties while
166
+ # warning/trend problems were correctly decayed. Return-
167
+ # shape drift archetype — sibling to.
168
+ return apply_property_confidence(
169
+ entity, indicator.property_name, problems
170
+ )
171
+
172
+ if warning_threshold is not None:
173
+ if current >= warning_threshold:
174
+ problems.append(Problem.from_entity(
175
+ entity=entity,
176
+ problem_type=f'threshold_warning:{indicator.name}',
177
+ severity=Severity.WARNING,
178
+ reason=f"{indicator.name} exceeds warning threshold",
179
+ axiom=Axiom.BOUNDEDNESS,
180
+ source_layer=DetectionLayer.ONTOLOGY,
181
+ evidence={
182
+ 'indicator': indicator.name,
183
+ 'value': current,
184
+ 'threshold': warning_threshold,
185
+ 'threshold_type': 'warning',
186
+ },
187
+ confidence=1.0,
188
+ ))
189
+
190
+ # 2. Check trend toward limits (requires history)
191
+ window = indicator.time_window or timedelta(hours=1)
192
+ values = history.get_values(entity.id, indicator.property_name, window)
193
+
194
+ if len(values) >= 5:
195
+ trend = calculate_trend(values)
196
+ slope = trend['slope']
197
+ r2 = trend['r2']
198
+
199
+ # Only consider significant positive trends
200
+ # Use threshold to prevent near-zero slopes from
201
+ # producing astronomically large time-to-critical estimates
202
+ # Use relative slope minimum based on current value
203
+ # so large-value domains (e.g., temperatures in Kelvin) aren't
204
+ # over-sensitive to tiny absolute slopes.
205
+ slope_minimum = max(1e-6, abs(current) * 1e-4) if current else 1e-6
206
+ if slope > slope_minimum and r2 >= self.params.boundedness_trend_min_r2:
207
+ # Estimate time to critical — uses the same
208
+ # effective threshold the immediate-violation branch
209
+ # consulted above, so trend-to-limit projection respects
210
+ # overlay overrides.
211
+ if critical_threshold and current < critical_threshold:
212
+ time_to_critical = (critical_threshold - current) / slope
213
+ threshold_hours = self.params.boundedness_time_to_critical_hours * 3600
214
+
215
+ if time_to_critical < threshold_hours:
216
+ problems.append(Problem.from_entity(
217
+ entity=entity,
218
+ problem_type=f'approaching_limit:{indicator.name}',
219
+ severity=Severity.HIGH,
220
+ reason=f"{indicator.name} trending toward critical limit",
221
+ axiom=Axiom.BOUNDEDNESS,
222
+ source_layer=DetectionLayer.ONTOLOGY,
223
+ evidence={
224
+ 'indicator': indicator.name,
225
+ 'value': current,
226
+ 'critical_threshold': critical_threshold,
227
+ 'slope': slope,
228
+ 'r2': r2,
229
+ 'time_to_critical_seconds': time_to_critical,
230
+ 'observations': len(values),
231
+ },
232
+ confidence=r2,
233
+ recommended_action=f"Expected to reach critical in {time_to_critical/60:.0f} minutes",
234
+ ))
235
+
236
+ # fire counting moved to the dispatch boundary
237
+ # (reasoner._record_fires), so all eight axioms are counted
238
+ # uniformly rather than three by hand.
239
+ result = apply_property_confidence(
240
+ entity, indicator.property_name, problems)
241
+
242
+ # BOUNDEDNESS has two arms — threshold comparison and trend
243
+ # projection — and both can be skipped. With neither threshold
244
+ # configured and fewer than 5 observations, this return is reached
245
+ # having evaluated nothing.
246
+ #
247
+ # The threshold tests are `is not None` rather than truthiness, so a
248
+ # legitimate threshold of 0.0 is honoured; that is why this condition
249
+ # mirrors them exactly instead of using `not critical_threshold`.
250
+ if (critical_threshold is None and warning_threshold is None
251
+ and len(values) < 5):
252
+ return CheckOutcome(result).declined(
253
+ Axiom.BOUNDEDNESS, entity, indicator.name,
254
+ NotEvaluatedReason.NO_THRESHOLD,
255
+ detail=(
256
+ "no warning or critical threshold configured, and too "
257
+ "few observations to project a trend"),
258
+ observations_count=len(values),
259
+ required_count=5,
260
+ )
261
+ return result
262
+
263
+ def check_capacity_ratio(
264
+ self,
265
+ entity: Entity,
266
+ used_property: str,
267
+ limit_property: str
268
+ ) -> List[Problem]:
269
+ """
270
+ Check capacity ratio (used/limit).
271
+
272
+ Useful for resources like CPU, memory, disk.
273
+ """
274
+ problems = []
275
+
276
+ used = entity.get_property(used_property)
277
+ limit = entity.get_property(limit_property)
278
+
279
+ if used is None or limit is None:
280
+ return problems
281
+
282
+ try:
283
+ used = float(used)
284
+ limit = float(limit)
285
+ except (TypeError, ValueError):
286
+ return problems
287
+
288
+ if limit <= 0:
289
+ return problems
290
+
291
+ ratio = used / limit
292
+
293
+ # resolve per-entity-axiom override (sentinel key)
294
+ # before falling back to global AxiomParameters. ``used_property``
295
+ # serves as the indicator name in the (entity, indicator, axiom)
296
+ # lookup tuple. When no override is present, behavior is identical
297
+ # to previously — fallback unwrapped is the global params scalar.
298
+ warn_ratio, critical_ratio = resolve_axiom_threshold(
299
+ entity, used_property, "BOUNDEDNESS",
300
+ fallback=(
301
+ self.params.boundedness_warning_ratio,
302
+ self.params.boundedness_critical_ratio,
303
+ ),
304
+ bound="both",
305
+ )
306
+
307
+ if ratio >= critical_ratio:
308
+ problems.append(Problem.from_entity(
309
+ entity=entity,
310
+ problem_type='capacity_exhausted',
311
+ severity=Severity.CRITICAL,
312
+ reason=f"Capacity near exhaustion ({ratio*100:.1f}%)",
313
+ axiom=Axiom.BOUNDEDNESS,
314
+ source_layer=DetectionLayer.ONTOLOGY,
315
+ evidence={
316
+ 'used': used,
317
+ 'limit': limit,
318
+ 'ratio': ratio,
319
+ 'threshold': critical_ratio,
320
+ },
321
+ confidence=1.0,
322
+ ))
323
+ elif ratio >= warn_ratio:
324
+ problems.append(Problem.from_entity(
325
+ entity=entity,
326
+ problem_type='capacity_warning',
327
+ severity=Severity.WARNING,
328
+ reason=f"Capacity at {ratio*100:.1f}%",
329
+ axiom=Axiom.BOUNDEDNESS,
330
+ source_layer=DetectionLayer.ONTOLOGY,
331
+ evidence={
332
+ 'used': used,
333
+ 'limit': limit,
334
+ 'ratio': ratio,
335
+ 'threshold': warn_ratio,
336
+ },
337
+ confidence=1.0,
338
+ ))
339
+
340
+ return problems