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.
- arbiter_engine/__init__.py +31 -0
- arbiter_engine/api.py +473 -0
- arbiter_engine/axiom_thresholds.py +118 -0
- arbiter_engine/envelope.py +270 -0
- arbiter_engine/examples/water_tank.yaml +112 -0
- arbiter_engine/fire_frequency.py +252 -0
- arbiter_engine/history/__init__.py +12 -0
- arbiter_engine/history/observation.py +596 -0
- arbiter_engine/history/observation_production.py +335 -0
- arbiter_engine/history/observation_source_wiring.py +120 -0
- arbiter_engine/history/readiness.py +288 -0
- arbiter_engine/interfaces.py +1353 -0
- arbiter_engine/mcp/__init__.py +1 -0
- arbiter_engine/mcp/server.py +221 -0
- arbiter_engine/ontology/__init__.py +18 -0
- arbiter_engine/ontology/axiom_verdicts_production.py +332 -0
- arbiter_engine/ontology/axioms/__init__.py +26 -0
- arbiter_engine/ontology/axioms/boundedness.py +340 -0
- arbiter_engine/ontology/axioms/connectivity.py +396 -0
- arbiter_engine/ontology/axioms/conservation.py +266 -0
- arbiter_engine/ontology/axioms/consistency.py +422 -0
- arbiter_engine/ontology/axioms/extensions.py +106 -0
- arbiter_engine/ontology/axioms/homeostasis.py +783 -0
- arbiter_engine/ontology/axioms/monotonicity.py +444 -0
- arbiter_engine/ontology/axioms/responsiveness.py +752 -0
- arbiter_engine/ontology/axioms/roles.py +207 -0
- arbiter_engine/ontology/axioms/stability.py +327 -0
- arbiter_engine/ontology/domain_loader.py +485 -0
- arbiter_engine/ontology/loader.py +802 -0
- arbiter_engine/ontology/reasoner.py +1069 -0
- arbiter_engine/propagation/__init__.py +1 -0
- arbiter_engine/propagation/impact_estimator.py +305 -0
- arbiter_engine/propagation/lp_confidence.py +123 -0
- arbiter_engine/propagation/mcts_root_cause.py +266 -0
- arbiter_engine/propagation/root_cause.py +559 -0
- arbiter_engine/propagation/weight_learner.py +216 -0
- arbiter_engine/rca/__init__.py +4 -0
- arbiter_engine/rca/greedy_set_cover.py +291 -0
- arbiter_engine/residual/__init__.py +1 -0
- arbiter_engine/residual/predict_vs_mirror.py +542 -0
- arbiter_engine/temporal/__init__.py +1 -0
- arbiter_engine/temporal/temporal_edge.py +584 -0
- arbiter_engine/temporal/trend_projection.py +430 -0
- arbiter_engine/twin/__init__.py +23 -0
- arbiter_engine/twin/action_clears_problem.py +217 -0
- arbiter_engine/twin/builder.py +479 -0
- arbiter_engine/twin/gap.py +132 -0
- arbiter_engine/twin/hypothesis_generator.py +530 -0
- arbiter_engine/twin/hypothesis_production.py +236 -0
- arbiter_engine/twin/kernel_pipeline_executor.py +571 -0
- arbiter_engine/twin/monte_carlo_predictor.py +1001 -0
- arbiter_engine/twin/optimization_production.py +233 -0
- arbiter_engine/twin/pipeline_production.py +176 -0
- arbiter_engine/twin/topology.py +450 -0
- arbiter_engine/twin/topology_optimizer.py +412 -0
- arbiter_engine/twin/traverser.py +1260 -0
- arbiter_engine/twin/traverser_production.py +283 -0
- arbiter_engine/types.py +427 -0
- arbiter_engine-0.1.0.dist-info/METADATA +278 -0
- arbiter_engine-0.1.0.dist-info/RECORD +63 -0
- arbiter_engine-0.1.0.dist-info/WHEEL +4 -0
- arbiter_engine-0.1.0.dist-info/licenses/LICENSE +202 -0
- 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
|