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.

Files changed (85) hide show
  1. {margin-0.9.6/margin.egg-info → margin-0.9.7}/PKG-INFO +1 -1
  2. {margin-0.9.6 → margin-0.9.7}/margin/config.py +11 -1
  3. {margin-0.9.6 → margin-0.9.7}/margin/contract.py +7 -0
  4. {margin-0.9.6 → margin-0.9.7}/margin/ledger.py +4 -4
  5. {margin-0.9.6 → margin-0.9.7}/margin/loop.py +13 -8
  6. {margin-0.9.6 → margin-0.9.7}/margin/observation.py +6 -3
  7. {margin-0.9.6 → margin-0.9.7/margin.egg-info}/PKG-INFO +1 -1
  8. {margin-0.9.6 → margin-0.9.7}/pyproject.toml +1 -1
  9. {margin-0.9.6 → margin-0.9.7}/tests/test_config.py +56 -0
  10. {margin-0.9.6 → margin-0.9.7}/tests/test_contract.py +21 -0
  11. {margin-0.9.6 → margin-0.9.7}/tests/test_ledger.py +24 -0
  12. {margin-0.9.6 → margin-0.9.7}/LICENSE +0 -0
  13. {margin-0.9.6 → margin-0.9.7}/NOTICE +0 -0
  14. {margin-0.9.6 → margin-0.9.7}/README.md +0 -0
  15. {margin-0.9.6 → margin-0.9.7}/margin/__init__.py +0 -0
  16. {margin-0.9.6 → margin-0.9.7}/margin/__main__.py +0 -0
  17. {margin-0.9.6 → margin-0.9.7}/margin/algebra.py +0 -0
  18. {margin-0.9.6 → margin-0.9.7}/margin/anomaly.py +0 -0
  19. {margin-0.9.6 → margin-0.9.7}/margin/bridge.py +0 -0
  20. {margin-0.9.6 → margin-0.9.7}/margin/calibrate.py +0 -0
  21. {margin-0.9.6 → margin-0.9.7}/margin/causal.py +0 -0
  22. {margin-0.9.6 → margin-0.9.7}/margin/composite.py +0 -0
  23. {margin-0.9.6 → margin-0.9.7}/margin/confidence.py +0 -0
  24. {margin-0.9.6 → margin-0.9.7}/margin/correlate.py +0 -0
  25. {margin-0.9.6 → margin-0.9.7}/margin/diff.py +0 -0
  26. {margin-0.9.6 → margin-0.9.7}/margin/drift.py +0 -0
  27. {margin-0.9.6 → margin-0.9.7}/margin/events.py +0 -0
  28. {margin-0.9.6 → margin-0.9.7}/margin/forecast.py +0 -0
  29. {margin-0.9.6 → margin-0.9.7}/margin/health.py +0 -0
  30. {margin-0.9.6 → margin-0.9.7}/margin/intent.py +0 -0
  31. {margin-0.9.6 → margin-0.9.7}/margin/persist.py +0 -0
  32. {margin-0.9.6 → margin-0.9.7}/margin/policy/__init__.py +0 -0
  33. {margin-0.9.6 → margin-0.9.7}/margin/policy/compose.py +0 -0
  34. {margin-0.9.6 → margin-0.9.7}/margin/policy/core.py +0 -0
  35. {margin-0.9.6 → margin-0.9.7}/margin/policy/temporal.py +0 -0
  36. {margin-0.9.6 → margin-0.9.7}/margin/policy/trace.py +0 -0
  37. {margin-0.9.6 → margin-0.9.7}/margin/policy/tuning.py +0 -0
  38. {margin-0.9.6 → margin-0.9.7}/margin/policy/validate.py +0 -0
  39. {margin-0.9.6 → margin-0.9.7}/margin/predicates.py +0 -0
  40. {margin-0.9.6 → margin-0.9.7}/margin/provenance.py +0 -0
  41. {margin-0.9.6 → margin-0.9.7}/margin/streaming.py +0 -0
  42. {margin-0.9.6 → margin-0.9.7}/margin/transitions.py +0 -0
  43. {margin-0.9.6 → margin-0.9.7}/margin/uncertain.py +0 -0
  44. {margin-0.9.6 → margin-0.9.7}/margin/validity.py +0 -0
  45. {margin-0.9.6 → margin-0.9.7}/margin.egg-info/SOURCES.txt +0 -0
  46. {margin-0.9.6 → margin-0.9.7}/margin.egg-info/dependency_links.txt +0 -0
  47. {margin-0.9.6 → margin-0.9.7}/margin.egg-info/top_level.txt +0 -0
  48. {margin-0.9.6 → margin-0.9.7}/setup.cfg +0 -0
  49. {margin-0.9.6 → margin-0.9.7}/tests/test_algebra.py +0 -0
  50. {margin-0.9.6 → margin-0.9.7}/tests/test_anomaly.py +0 -0
  51. {margin-0.9.6 → margin-0.9.7}/tests/test_bridge.py +0 -0
  52. {margin-0.9.6 → margin-0.9.7}/tests/test_calibrate.py +0 -0
  53. {margin-0.9.6 → margin-0.9.7}/tests/test_causal.py +0 -0
  54. {margin-0.9.6 → margin-0.9.7}/tests/test_composite.py +0 -0
  55. {margin-0.9.6 → margin-0.9.7}/tests/test_confidence.py +0 -0
  56. {margin-0.9.6 → margin-0.9.7}/tests/test_correlate.py +0 -0
  57. {margin-0.9.6 → margin-0.9.7}/tests/test_diff.py +0 -0
  58. {margin-0.9.6 → margin-0.9.7}/tests/test_drift.py +0 -0
  59. {margin-0.9.6 → margin-0.9.7}/tests/test_events.py +0 -0
  60. {margin-0.9.6 → margin-0.9.7}/tests/test_forecast.py +0 -0
  61. {margin-0.9.6 → margin-0.9.7}/tests/test_health.py +0 -0
  62. {margin-0.9.6 → margin-0.9.7}/tests/test_healthcare.py +0 -0
  63. {margin-0.9.6 → margin-0.9.7}/tests/test_intent.py +0 -0
  64. {margin-0.9.6 → margin-0.9.7}/tests/test_loop.py +0 -0
  65. {margin-0.9.6 → margin-0.9.7}/tests/test_neuro.py +0 -0
  66. {margin-0.9.6 → margin-0.9.7}/tests/test_observation.py +0 -0
  67. {margin-0.9.6 → margin-0.9.7}/tests/test_persist.py +0 -0
  68. {margin-0.9.6 → margin-0.9.7}/tests/test_policy.py +0 -0
  69. {margin-0.9.6 → margin-0.9.7}/tests/test_policy_compose.py +0 -0
  70. {margin-0.9.6 → margin-0.9.7}/tests/test_policy_temporal.py +0 -0
  71. {margin-0.9.6 → margin-0.9.7}/tests/test_policy_trace.py +0 -0
  72. {margin-0.9.6 → margin-0.9.7}/tests/test_policy_tuning.py +0 -0
  73. {margin-0.9.6 → margin-0.9.7}/tests/test_policy_validate.py +0 -0
  74. {margin-0.9.6 → margin-0.9.7}/tests/test_predicates.py +0 -0
  75. {margin-0.9.6 → margin-0.9.7}/tests/test_printer3d.py +0 -0
  76. {margin-0.9.6 → margin-0.9.7}/tests/test_provenance.py +0 -0
  77. {margin-0.9.6 → margin-0.9.7}/tests/test_reverse_bridge.py +0 -0
  78. {margin-0.9.6 → margin-0.9.7}/tests/test_ros2.py +0 -0
  79. {margin-0.9.6 → margin-0.9.7}/tests/test_staleness.py +0 -0
  80. {margin-0.9.6 → margin-0.9.7}/tests/test_streaming.py +0 -0
  81. {margin-0.9.6 → margin-0.9.7}/tests/test_transformer.py +0 -0
  82. {margin-0.9.6 → margin-0.9.7}/tests/test_transitions.py +0 -0
  83. {margin-0.9.6 → margin-0.9.7}/tests/test_uncertain.py +0 -0
  84. {margin-0.9.6 → margin-0.9.7}/tests/test_validity.py +0 -0
  85. {margin-0.9.6 → margin-0.9.7}/tests/test_windowing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.9.6
3
+ Version: 0.9.7
4
4
  Summary: Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened.
5
5
  Author: Cope Labs LLC
6
6
  License: MIT
@@ -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(name=config.get("name", "config-policy"), rules=rules)
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: streaming Monitor
296
- values: raw measurements {component: value}
297
- policy: decision policy
298
- ledger: correction ledger (optional)
299
- graph: causal graph (optional)
300
- contract: success contract (optional)
301
- intent: Intent goal (optional)
302
- now: current timestamp (optional)
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: {component_name: measurement}
336
+ values: {component_name: measurement}
337
337
  correction_magnitude: size of the correction being applied
338
- alpha: mixing coefficient for the correction
339
- confidences: per-component confidence (defaults to MODERATE)
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 []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.9.6
3
+ Version: 0.9.7
4
4
  Summary: Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened.
5
5
  Author: Cope Labs LLC
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "margin"
7
- version = "0.9.6"
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