margin 0.9.6__tar.gz → 0.9.8__tar.gz
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.
Potentially problematic release.
This version of margin might be problematic. Click here for more details.
- {margin-0.9.6/margin.egg-info → margin-0.9.8}/PKG-INFO +1 -1
- {margin-0.9.6 → margin-0.9.8}/margin/config.py +11 -1
- {margin-0.9.6 → margin-0.9.8}/margin/contract.py +7 -0
- {margin-0.9.6 → margin-0.9.8}/margin/ledger.py +4 -4
- {margin-0.9.6 → margin-0.9.8}/margin/loop.py +13 -8
- {margin-0.9.6 → margin-0.9.8}/margin/observation.py +6 -3
- {margin-0.9.6 → margin-0.9.8}/margin/policy/validate.py +26 -0
- {margin-0.9.6 → margin-0.9.8}/margin/streaming.py +15 -0
- {margin-0.9.6 → margin-0.9.8/margin.egg-info}/PKG-INFO +1 -1
- {margin-0.9.6 → margin-0.9.8}/pyproject.toml +1 -1
- {margin-0.9.6 → margin-0.9.8}/tests/test_config.py +56 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_contract.py +21 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_ledger.py +24 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_policy_validate.py +38 -0
- {margin-0.9.6 → margin-0.9.8}/LICENSE +0 -0
- {margin-0.9.6 → margin-0.9.8}/NOTICE +0 -0
- {margin-0.9.6 → margin-0.9.8}/README.md +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/__init__.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/__main__.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/algebra.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/anomaly.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/bridge.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/calibrate.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/causal.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/composite.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/confidence.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/correlate.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/diff.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/drift.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/events.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/forecast.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/health.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/intent.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/persist.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/policy/__init__.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/policy/compose.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/policy/core.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/policy/temporal.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/policy/trace.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/policy/tuning.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/predicates.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/provenance.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/transitions.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/uncertain.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin/validity.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin.egg-info/SOURCES.txt +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin.egg-info/dependency_links.txt +0 -0
- {margin-0.9.6 → margin-0.9.8}/margin.egg-info/top_level.txt +0 -0
- {margin-0.9.6 → margin-0.9.8}/setup.cfg +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_algebra.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_anomaly.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_bridge.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_calibrate.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_causal.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_composite.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_confidence.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_correlate.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_diff.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_drift.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_events.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_forecast.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_health.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_healthcare.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_intent.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_loop.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_neuro.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_observation.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_persist.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_policy.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_policy_compose.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_policy_temporal.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_policy_trace.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_policy_tuning.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_predicates.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_printer3d.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_provenance.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_reverse_bridge.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_ros2.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_staleness.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_streaming.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_transformer.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_transitions.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_uncertain.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_validity.py +0 -0
- {margin-0.9.6 → margin-0.9.8}/tests/test_windowing.py +0 -0
|
@@ -125,6 +125,7 @@ def from_config(config: dict) -> dict:
|
|
|
125
125
|
intact=spec.get("intact", default_intact),
|
|
126
126
|
ablated=spec.get("ablated", default_ablated),
|
|
127
127
|
higher_is_better=hib,
|
|
128
|
+
labels=spec.get("labels"),
|
|
128
129
|
)
|
|
129
130
|
|
|
130
131
|
# Use first component's thresholds as default (or the explicit default)
|
|
@@ -153,6 +154,8 @@ def from_config(config: dict) -> dict:
|
|
|
153
154
|
op=Op(action_spec.get("op", "RESTORE")),
|
|
154
155
|
alpha=action_spec.get("alpha", 0.5),
|
|
155
156
|
magnitude=action_spec.get("magnitude", 1.0),
|
|
157
|
+
alpha_from_sigma=action_spec.get("alpha_from_sigma", False),
|
|
158
|
+
magnitude_from_sigma=action_spec.get("magnitude_from_sigma", False),
|
|
156
159
|
)
|
|
157
160
|
constraint = None
|
|
158
161
|
if "constraint" in spec:
|
|
@@ -171,6 +174,8 @@ def from_config(config: dict) -> dict:
|
|
|
171
174
|
level=EscalationLevel(es.get("level", "LOG")),
|
|
172
175
|
reason=es.get("reason", ""),
|
|
173
176
|
)
|
|
177
|
+
min_conf_str = spec.get("min_confidence")
|
|
178
|
+
min_confidence = Confidence(min_conf_str) if min_conf_str else Confidence.LOW
|
|
174
179
|
rules.append(PolicyRule(
|
|
175
180
|
name=spec["name"],
|
|
176
181
|
predicate=predicate,
|
|
@@ -178,8 +183,13 @@ def from_config(config: dict) -> dict:
|
|
|
178
183
|
priority=spec.get("priority", 0),
|
|
179
184
|
constraint=constraint,
|
|
180
185
|
escalation=escalation,
|
|
186
|
+
min_confidence=min_confidence,
|
|
181
187
|
))
|
|
182
|
-
result["policy"] = Policy(
|
|
188
|
+
result["policy"] = Policy(
|
|
189
|
+
name=config.get("name", "config-policy"),
|
|
190
|
+
rules=rules,
|
|
191
|
+
multi_rule=config.get("multi_rule", False),
|
|
192
|
+
)
|
|
183
193
|
|
|
184
194
|
# --- Contract ---
|
|
185
195
|
contract_specs = config.get("contract", [])
|
|
@@ -352,5 +352,12 @@ class Contract:
|
|
|
352
352
|
def to_dict(self) -> dict:
|
|
353
353
|
return {"name": self.name, "terms": [t.to_dict() for t in self.terms]}
|
|
354
354
|
|
|
355
|
+
@classmethod
|
|
356
|
+
def from_dict(cls, d: dict) -> 'Contract':
|
|
357
|
+
return cls(
|
|
358
|
+
name=d.get("name", ""),
|
|
359
|
+
terms=[contract_term_from_dict(t) for t in d.get("terms", [])],
|
|
360
|
+
)
|
|
361
|
+
|
|
355
362
|
def __repr__(self) -> str:
|
|
356
363
|
return f"Contract({self.name!r}, {len(self.terms)} terms)"
|
|
@@ -211,11 +211,11 @@ class Ledger:
|
|
|
211
211
|
"n_harmful": len(self.harmful()),
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
def to_dict(self) -> dict:
|
|
215
|
+
return {"label": self.label, "records": [r.to_dict() for r in self.records]}
|
|
216
|
+
|
|
214
217
|
def to_json(self, indent: int = 2) -> str:
|
|
215
|
-
return json.dumps(
|
|
216
|
-
"label": self.label,
|
|
217
|
-
"records": [r.to_dict() for r in self.records],
|
|
218
|
-
}, indent=indent)
|
|
218
|
+
return json.dumps(self.to_dict(), indent=indent)
|
|
219
219
|
|
|
220
220
|
@classmethod
|
|
221
221
|
def from_dict(cls, d: dict) -> 'Ledger':
|
|
@@ -250,6 +250,8 @@ class FullStepResult:
|
|
|
250
250
|
d["drift"] = {k: v.to_dict() for k, v in self.drift.items()}
|
|
251
251
|
if self.anomaly:
|
|
252
252
|
d["anomaly"] = {k: v.to_dict() for k, v in self.anomaly.items()}
|
|
253
|
+
if self.correlations is not None and self.correlations.to_dict():
|
|
254
|
+
d["correlations"] = self.correlations.to_dict()
|
|
253
255
|
if self.intent:
|
|
254
256
|
d["intent"] = self.intent.to_dict()
|
|
255
257
|
return d
|
|
@@ -292,14 +294,17 @@ def full_step(
|
|
|
292
294
|
3. Intent.evaluate() → goal feasibility
|
|
293
295
|
|
|
294
296
|
Args:
|
|
295
|
-
monitor:
|
|
296
|
-
values:
|
|
297
|
-
policy:
|
|
298
|
-
ledger:
|
|
299
|
-
graph:
|
|
300
|
-
contract:
|
|
301
|
-
intent:
|
|
302
|
-
now:
|
|
297
|
+
monitor: streaming Monitor
|
|
298
|
+
values: raw measurements {component: value}
|
|
299
|
+
policy: decision policy
|
|
300
|
+
ledger: correction ledger (optional)
|
|
301
|
+
graph: causal graph (optional)
|
|
302
|
+
contract: success contract (optional)
|
|
303
|
+
intent: Intent goal (optional)
|
|
304
|
+
now: current timestamp (optional)
|
|
305
|
+
confidences: per-component Confidence overrides (optional)
|
|
306
|
+
provenance: provenance tags for all observations (optional)
|
|
307
|
+
label: expression label for this step (optional)
|
|
303
308
|
|
|
304
309
|
Returns FullStepResult with all stages evaluated.
|
|
305
310
|
"""
|
|
@@ -333,10 +333,13 @@ class Parser:
|
|
|
333
333
|
"""
|
|
334
334
|
Parse a set of component measurements into a typed Expression.
|
|
335
335
|
|
|
336
|
-
values:
|
|
336
|
+
values: {component_name: measurement}
|
|
337
337
|
correction_magnitude: size of the correction being applied
|
|
338
|
-
alpha:
|
|
339
|
-
confidences:
|
|
338
|
+
alpha: mixing coefficient for the correction
|
|
339
|
+
confidences: per-component Confidence overrides (defaults to MODERATE)
|
|
340
|
+
label: tag for this expression (e.g. step identifier)
|
|
341
|
+
step: step index (set automatically by Monitor)
|
|
342
|
+
provenance: provenance tags attached to all observations
|
|
340
343
|
"""
|
|
341
344
|
confidences = confidences or {}
|
|
342
345
|
provenance = provenance or []
|
|
@@ -82,6 +82,10 @@ def validate(
|
|
|
82
82
|
5. Missing health coverage (no rule handles ABLATED, DEGRADED, etc.)
|
|
83
83
|
6. Contract coverage (if a contract is provided, check that the
|
|
84
84
|
policy has rules for every component the contract mentions)
|
|
85
|
+
7. Component coverage (if components list provided)
|
|
86
|
+
8. alpha_from_sigma without a constraint floor
|
|
87
|
+
9. min_confidence too high (Parser.parse() defaults to MODERATE, so
|
|
88
|
+
rules with min_confidence >= HIGH may silently never fire)
|
|
85
89
|
|
|
86
90
|
Args:
|
|
87
91
|
policy: the policy to validate
|
|
@@ -146,6 +150,9 @@ def validate(
|
|
|
146
150
|
# 8. alpha_from_sigma without a constraint floor
|
|
147
151
|
_check_alpha_from_sigma(policy, issues)
|
|
148
152
|
|
|
153
|
+
# 9. min_confidence gate too high for default parse behaviour
|
|
154
|
+
_check_min_confidence(policy, issues)
|
|
155
|
+
|
|
149
156
|
return ValidationResult(issues=issues)
|
|
150
157
|
|
|
151
158
|
|
|
@@ -256,3 +263,22 @@ def _check_component_coverage(
|
|
|
256
263
|
issues.append(ValidationIssue(
|
|
257
264
|
"info", "policy",
|
|
258
265
|
f"component '{comp}' has no dedicated rule (no wildcard rules either)"))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _check_min_confidence(policy: Policy, issues: list[ValidationIssue]) -> None:
|
|
269
|
+
"""
|
|
270
|
+
Warn when a rule's min_confidence gate is above MODERATE.
|
|
271
|
+
|
|
272
|
+
Parser.parse() assigns MODERATE confidence to every observation by
|
|
273
|
+
default (when no per-component confidences dict is passed). A rule
|
|
274
|
+
with min_confidence >= HIGH will therefore silently never fire unless
|
|
275
|
+
the caller explicitly passes HIGH or CERTAIN confidences on every step.
|
|
276
|
+
"""
|
|
277
|
+
for rule in policy.rules:
|
|
278
|
+
if rule.min_confidence >= Confidence.HIGH:
|
|
279
|
+
issues.append(ValidationIssue(
|
|
280
|
+
"warning", rule.name,
|
|
281
|
+
f"min_confidence={rule.min_confidence.value} — Parser.parse() defaults "
|
|
282
|
+
"to MODERATE; this rule will never fire unless the caller explicitly "
|
|
283
|
+
"passes per-component confidences of HIGH or CERTAIN",
|
|
284
|
+
))
|
|
@@ -392,6 +392,21 @@ class Monitor:
|
|
|
392
392
|
anomaly_min_reference: int = 10,
|
|
393
393
|
features: Optional[set] = None,
|
|
394
394
|
):
|
|
395
|
+
"""
|
|
396
|
+
Args:
|
|
397
|
+
parser: Parser configured with baselines and thresholds
|
|
398
|
+
window: base window size for all trackers (overridden per-tracker)
|
|
399
|
+
drift_window: DriftTracker window (overrides ``window``)
|
|
400
|
+
anomaly_window: AnomalyTracker window (overrides ``window``)
|
|
401
|
+
correlation_window: CorrelationTracker window (overrides ``window``)
|
|
402
|
+
window_config: WindowConfig dataclass; overrides named window params
|
|
403
|
+
min_correlation: minimum |r| to include a pair in correlations
|
|
404
|
+
anomaly_min_reference: minimum steps before AnomalyTracker classifies
|
|
405
|
+
features: set of tracker names to enable — ``{"drift"``,
|
|
406
|
+
``"anomaly"``, ``"correlation"}``; ``None`` (default)
|
|
407
|
+
enables all three; ``set()`` disables all.
|
|
408
|
+
``"health"`` is always implied and need not be listed.
|
|
409
|
+
"""
|
|
395
410
|
self.parser = parser
|
|
396
411
|
self.window = window
|
|
397
412
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "margin"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.8"
|
|
8
8
|
description = "Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -244,3 +244,59 @@ class TestConfigConstraintEscalation:
|
|
|
244
244
|
}
|
|
245
245
|
result = from_config(cfg)
|
|
246
246
|
assert isinstance(result["contract"].terms[0], HealthTarget)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class TestConfigNewFields:
|
|
250
|
+
def test_alpha_from_sigma_in_config(self):
|
|
251
|
+
from margin import from_config
|
|
252
|
+
cfg = {
|
|
253
|
+
"components": {"cpu": {"baseline": 50.0, "intact": 40.0, "ablated": 10.0}},
|
|
254
|
+
"policy": [{
|
|
255
|
+
"name": "dynamic",
|
|
256
|
+
"when": "any_degraded",
|
|
257
|
+
"action": {"op": "RESTORE", "alpha_from_sigma": True},
|
|
258
|
+
}],
|
|
259
|
+
}
|
|
260
|
+
result = from_config(cfg)
|
|
261
|
+
assert result["policy"].rules[0].action.alpha_from_sigma is True
|
|
262
|
+
|
|
263
|
+
def test_min_confidence_in_config(self):
|
|
264
|
+
from margin import from_config
|
|
265
|
+
from margin.confidence import Confidence
|
|
266
|
+
cfg = {
|
|
267
|
+
"components": {"cpu": {"baseline": 50.0, "intact": 40.0, "ablated": 10.0}},
|
|
268
|
+
"policy": [{
|
|
269
|
+
"name": "high-conf-only",
|
|
270
|
+
"when": "any_degraded",
|
|
271
|
+
"action": {"op": "RESTORE", "alpha": 0.5},
|
|
272
|
+
"min_confidence": "high",
|
|
273
|
+
}],
|
|
274
|
+
}
|
|
275
|
+
result = from_config(cfg)
|
|
276
|
+
assert result["policy"].rules[0].min_confidence == Confidence.HIGH
|
|
277
|
+
|
|
278
|
+
def test_multi_rule_in_config(self):
|
|
279
|
+
from margin import from_config
|
|
280
|
+
cfg = {
|
|
281
|
+
"name": "my-policy",
|
|
282
|
+
"multi_rule": True,
|
|
283
|
+
"components": {"cpu": {"baseline": 50.0, "intact": 40.0, "ablated": 10.0}},
|
|
284
|
+
"policy": [{"name": "r", "when": "any_degraded", "action": {"op": "RESTORE"}}],
|
|
285
|
+
}
|
|
286
|
+
result = from_config(cfg)
|
|
287
|
+
assert result["policy"].multi_rule is True
|
|
288
|
+
|
|
289
|
+
def test_labels_in_config(self):
|
|
290
|
+
from margin import from_config
|
|
291
|
+
cfg = {
|
|
292
|
+
"components": {
|
|
293
|
+
"cpu": {
|
|
294
|
+
"baseline": 50.0, "intact": 40.0, "ablated": 10.0,
|
|
295
|
+
"labels": {"ABLATED": "CRITICAL", "INTACT": "OK"},
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
}
|
|
299
|
+
result = from_config(cfg)
|
|
300
|
+
from margin.health import Health
|
|
301
|
+
assert result["parser"].label_for("cpu", Health.ABLATED) == "CRITICAL"
|
|
302
|
+
assert result["parser"].label_for("cpu", Health.INTACT) == "OK"
|
|
@@ -272,3 +272,24 @@ class TestContractTermFromDict:
|
|
|
272
272
|
from margin import contract_term_from_dict
|
|
273
273
|
with pytest.raises(ValueError, match="Unknown"):
|
|
274
274
|
contract_term_from_dict({"type": "nonexistent", "name": "x"})
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class TestContractFromDict:
|
|
278
|
+
def test_roundtrip(self):
|
|
279
|
+
from margin import Contract, SustainHealth, HealthTarget
|
|
280
|
+
c = Contract(name="my-contract", terms=[
|
|
281
|
+
HealthTarget(name="h", component="cpu", target=Health.INTACT),
|
|
282
|
+
SustainHealth(name="s", component="cpu", target=Health.INTACT, for_steps=5),
|
|
283
|
+
])
|
|
284
|
+
c2 = Contract.from_dict(c.to_dict())
|
|
285
|
+
assert c2.name == "my-contract"
|
|
286
|
+
assert len(c2.terms) == 2
|
|
287
|
+
assert isinstance(c2.terms[0], HealthTarget)
|
|
288
|
+
assert isinstance(c2.terms[1], SustainHealth)
|
|
289
|
+
|
|
290
|
+
def test_empty_contract_roundtrip(self):
|
|
291
|
+
from margin import Contract
|
|
292
|
+
c = Contract(name="empty")
|
|
293
|
+
c2 = Contract.from_dict(c.to_dict())
|
|
294
|
+
assert c2.name == "empty"
|
|
295
|
+
assert c2.terms == []
|
|
@@ -197,3 +197,27 @@ class TestLedgerRoundtrip:
|
|
|
197
197
|
assert lr.records[0].op == Op.RESTORE
|
|
198
198
|
assert lr.records[1].after is None
|
|
199
199
|
assert lr.render() == ledger.render()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TestLedgerToDict:
|
|
203
|
+
def test_to_dict_roundtrip(self):
|
|
204
|
+
from margin.ledger import Ledger, Record
|
|
205
|
+
from margin.observation import Observation, Op
|
|
206
|
+
from margin.health import Health
|
|
207
|
+
from margin.confidence import Confidence
|
|
208
|
+
|
|
209
|
+
obs = Observation("cpu", Health.DEGRADED, 30.0, 50.0, Confidence.HIGH)
|
|
210
|
+
rec = Record(step=0, tag="s0", before=obs, fired=False, op=Op.NOOP, alpha=0.0)
|
|
211
|
+
ledger = Ledger(label="test", records=[rec])
|
|
212
|
+
d = ledger.to_dict()
|
|
213
|
+
assert d["label"] == "test"
|
|
214
|
+
assert len(d["records"]) == 1
|
|
215
|
+
ledger2 = Ledger.from_dict(d)
|
|
216
|
+
assert ledger2.label == "test"
|
|
217
|
+
assert len(ledger2.records) == 1
|
|
218
|
+
|
|
219
|
+
def test_to_json_uses_to_dict(self):
|
|
220
|
+
import json
|
|
221
|
+
from margin.ledger import Ledger
|
|
222
|
+
ledger = Ledger(label="x")
|
|
223
|
+
assert json.loads(ledger.to_json())["label"] == "x"
|
|
@@ -151,6 +151,44 @@ class TestComponentCoverage:
|
|
|
151
151
|
assert len(comp_issues) == 0
|
|
152
152
|
|
|
153
153
|
|
|
154
|
+
class TestMinConfidenceCheck:
|
|
155
|
+
def test_high_confidence_gate_warns(self):
|
|
156
|
+
rule = PolicyRule(
|
|
157
|
+
"r", any_degraded(),
|
|
158
|
+
Action(target="*", op=Op.RESTORE, alpha=0.5),
|
|
159
|
+
min_confidence=Confidence.HIGH,
|
|
160
|
+
)
|
|
161
|
+
result = validate(Policy(name="p", rules=[rule]))
|
|
162
|
+
assert any("min_confidence" in i.message or "MODERATE" in i.message
|
|
163
|
+
for i in result.warnings)
|
|
164
|
+
|
|
165
|
+
def test_certain_confidence_gate_warns(self):
|
|
166
|
+
rule = PolicyRule(
|
|
167
|
+
"r", any_degraded(),
|
|
168
|
+
Action(target="*", op=Op.RESTORE, alpha=0.5),
|
|
169
|
+
min_confidence=Confidence.CERTAIN,
|
|
170
|
+
)
|
|
171
|
+
result = validate(Policy(name="p", rules=[rule]))
|
|
172
|
+
assert any("min_confidence" in i.message or "MODERATE" in i.message
|
|
173
|
+
for i in result.warnings)
|
|
174
|
+
|
|
175
|
+
def test_moderate_confidence_gate_no_warn(self):
|
|
176
|
+
rule = PolicyRule(
|
|
177
|
+
"r", any_degraded(),
|
|
178
|
+
Action(target="*", op=Op.RESTORE, alpha=0.5),
|
|
179
|
+
min_confidence=Confidence.MODERATE,
|
|
180
|
+
)
|
|
181
|
+
result = validate(Policy(name="p", rules=[rule]))
|
|
182
|
+
# No warning about min_confidence for MODERATE (default parse behaviour)
|
|
183
|
+
assert not any("MODERATE" in i.message and "min_confidence" in i.message
|
|
184
|
+
for i in result.warnings)
|
|
185
|
+
|
|
186
|
+
def test_low_confidence_gate_no_warn(self):
|
|
187
|
+
rule = _make_rule("r", any_degraded()) # default min_confidence=LOW
|
|
188
|
+
result = validate(Policy(name="p", rules=[rule]))
|
|
189
|
+
assert not any("min_confidence" in i.message for i in result.warnings)
|
|
190
|
+
|
|
191
|
+
|
|
154
192
|
class TestValidationResult:
|
|
155
193
|
def test_valid_when_no_errors(self):
|
|
156
194
|
r = ValidationResult(issues=[])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|