margin 0.9.6__tar.gz → 0.9.7__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.7}/PKG-INFO +1 -1
- {margin-0.9.6 → margin-0.9.7}/margin/config.py +11 -1
- {margin-0.9.6 → margin-0.9.7}/margin/contract.py +7 -0
- {margin-0.9.6 → margin-0.9.7}/margin/ledger.py +4 -4
- {margin-0.9.6 → margin-0.9.7}/margin/loop.py +13 -8
- {margin-0.9.6 → margin-0.9.7}/margin/observation.py +6 -3
- {margin-0.9.6 → margin-0.9.7/margin.egg-info}/PKG-INFO +1 -1
- {margin-0.9.6 → margin-0.9.7}/pyproject.toml +1 -1
- {margin-0.9.6 → margin-0.9.7}/tests/test_config.py +56 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_contract.py +21 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_ledger.py +24 -0
- {margin-0.9.6 → margin-0.9.7}/LICENSE +0 -0
- {margin-0.9.6 → margin-0.9.7}/NOTICE +0 -0
- {margin-0.9.6 → margin-0.9.7}/README.md +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/__init__.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/__main__.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/algebra.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/anomaly.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/bridge.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/calibrate.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/causal.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/composite.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/confidence.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/correlate.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/diff.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/drift.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/events.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/forecast.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/health.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/intent.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/persist.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/policy/__init__.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/policy/compose.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/policy/core.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/policy/temporal.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/policy/trace.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/policy/tuning.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/policy/validate.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/predicates.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/provenance.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/streaming.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/transitions.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/uncertain.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin/validity.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin.egg-info/SOURCES.txt +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin.egg-info/dependency_links.txt +0 -0
- {margin-0.9.6 → margin-0.9.7}/margin.egg-info/top_level.txt +0 -0
- {margin-0.9.6 → margin-0.9.7}/setup.cfg +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_algebra.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_anomaly.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_bridge.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_calibrate.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_causal.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_composite.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_confidence.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_correlate.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_diff.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_drift.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_events.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_forecast.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_health.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_healthcare.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_intent.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_loop.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_neuro.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_observation.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_persist.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_policy.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_policy_compose.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_policy_temporal.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_policy_trace.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_policy_tuning.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_policy_validate.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_predicates.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_printer3d.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_provenance.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_reverse_bridge.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_ros2.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_staleness.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_streaming.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_transformer.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_transitions.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_uncertain.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/tests/test_validity.py +0 -0
- {margin-0.9.6 → margin-0.9.7}/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 []
|
|
@@ -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.7"
|
|
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"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|