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.

Files changed (85) hide show
  1. {margin-0.9.6/margin.egg-info → margin-0.9.8}/PKG-INFO +1 -1
  2. {margin-0.9.6 → margin-0.9.8}/margin/config.py +11 -1
  3. {margin-0.9.6 → margin-0.9.8}/margin/contract.py +7 -0
  4. {margin-0.9.6 → margin-0.9.8}/margin/ledger.py +4 -4
  5. {margin-0.9.6 → margin-0.9.8}/margin/loop.py +13 -8
  6. {margin-0.9.6 → margin-0.9.8}/margin/observation.py +6 -3
  7. {margin-0.9.6 → margin-0.9.8}/margin/policy/validate.py +26 -0
  8. {margin-0.9.6 → margin-0.9.8}/margin/streaming.py +15 -0
  9. {margin-0.9.6 → margin-0.9.8/margin.egg-info}/PKG-INFO +1 -1
  10. {margin-0.9.6 → margin-0.9.8}/pyproject.toml +1 -1
  11. {margin-0.9.6 → margin-0.9.8}/tests/test_config.py +56 -0
  12. {margin-0.9.6 → margin-0.9.8}/tests/test_contract.py +21 -0
  13. {margin-0.9.6 → margin-0.9.8}/tests/test_ledger.py +24 -0
  14. {margin-0.9.6 → margin-0.9.8}/tests/test_policy_validate.py +38 -0
  15. {margin-0.9.6 → margin-0.9.8}/LICENSE +0 -0
  16. {margin-0.9.6 → margin-0.9.8}/NOTICE +0 -0
  17. {margin-0.9.6 → margin-0.9.8}/README.md +0 -0
  18. {margin-0.9.6 → margin-0.9.8}/margin/__init__.py +0 -0
  19. {margin-0.9.6 → margin-0.9.8}/margin/__main__.py +0 -0
  20. {margin-0.9.6 → margin-0.9.8}/margin/algebra.py +0 -0
  21. {margin-0.9.6 → margin-0.9.8}/margin/anomaly.py +0 -0
  22. {margin-0.9.6 → margin-0.9.8}/margin/bridge.py +0 -0
  23. {margin-0.9.6 → margin-0.9.8}/margin/calibrate.py +0 -0
  24. {margin-0.9.6 → margin-0.9.8}/margin/causal.py +0 -0
  25. {margin-0.9.6 → margin-0.9.8}/margin/composite.py +0 -0
  26. {margin-0.9.6 → margin-0.9.8}/margin/confidence.py +0 -0
  27. {margin-0.9.6 → margin-0.9.8}/margin/correlate.py +0 -0
  28. {margin-0.9.6 → margin-0.9.8}/margin/diff.py +0 -0
  29. {margin-0.9.6 → margin-0.9.8}/margin/drift.py +0 -0
  30. {margin-0.9.6 → margin-0.9.8}/margin/events.py +0 -0
  31. {margin-0.9.6 → margin-0.9.8}/margin/forecast.py +0 -0
  32. {margin-0.9.6 → margin-0.9.8}/margin/health.py +0 -0
  33. {margin-0.9.6 → margin-0.9.8}/margin/intent.py +0 -0
  34. {margin-0.9.6 → margin-0.9.8}/margin/persist.py +0 -0
  35. {margin-0.9.6 → margin-0.9.8}/margin/policy/__init__.py +0 -0
  36. {margin-0.9.6 → margin-0.9.8}/margin/policy/compose.py +0 -0
  37. {margin-0.9.6 → margin-0.9.8}/margin/policy/core.py +0 -0
  38. {margin-0.9.6 → margin-0.9.8}/margin/policy/temporal.py +0 -0
  39. {margin-0.9.6 → margin-0.9.8}/margin/policy/trace.py +0 -0
  40. {margin-0.9.6 → margin-0.9.8}/margin/policy/tuning.py +0 -0
  41. {margin-0.9.6 → margin-0.9.8}/margin/predicates.py +0 -0
  42. {margin-0.9.6 → margin-0.9.8}/margin/provenance.py +0 -0
  43. {margin-0.9.6 → margin-0.9.8}/margin/transitions.py +0 -0
  44. {margin-0.9.6 → margin-0.9.8}/margin/uncertain.py +0 -0
  45. {margin-0.9.6 → margin-0.9.8}/margin/validity.py +0 -0
  46. {margin-0.9.6 → margin-0.9.8}/margin.egg-info/SOURCES.txt +0 -0
  47. {margin-0.9.6 → margin-0.9.8}/margin.egg-info/dependency_links.txt +0 -0
  48. {margin-0.9.6 → margin-0.9.8}/margin.egg-info/top_level.txt +0 -0
  49. {margin-0.9.6 → margin-0.9.8}/setup.cfg +0 -0
  50. {margin-0.9.6 → margin-0.9.8}/tests/test_algebra.py +0 -0
  51. {margin-0.9.6 → margin-0.9.8}/tests/test_anomaly.py +0 -0
  52. {margin-0.9.6 → margin-0.9.8}/tests/test_bridge.py +0 -0
  53. {margin-0.9.6 → margin-0.9.8}/tests/test_calibrate.py +0 -0
  54. {margin-0.9.6 → margin-0.9.8}/tests/test_causal.py +0 -0
  55. {margin-0.9.6 → margin-0.9.8}/tests/test_composite.py +0 -0
  56. {margin-0.9.6 → margin-0.9.8}/tests/test_confidence.py +0 -0
  57. {margin-0.9.6 → margin-0.9.8}/tests/test_correlate.py +0 -0
  58. {margin-0.9.6 → margin-0.9.8}/tests/test_diff.py +0 -0
  59. {margin-0.9.6 → margin-0.9.8}/tests/test_drift.py +0 -0
  60. {margin-0.9.6 → margin-0.9.8}/tests/test_events.py +0 -0
  61. {margin-0.9.6 → margin-0.9.8}/tests/test_forecast.py +0 -0
  62. {margin-0.9.6 → margin-0.9.8}/tests/test_health.py +0 -0
  63. {margin-0.9.6 → margin-0.9.8}/tests/test_healthcare.py +0 -0
  64. {margin-0.9.6 → margin-0.9.8}/tests/test_intent.py +0 -0
  65. {margin-0.9.6 → margin-0.9.8}/tests/test_loop.py +0 -0
  66. {margin-0.9.6 → margin-0.9.8}/tests/test_neuro.py +0 -0
  67. {margin-0.9.6 → margin-0.9.8}/tests/test_observation.py +0 -0
  68. {margin-0.9.6 → margin-0.9.8}/tests/test_persist.py +0 -0
  69. {margin-0.9.6 → margin-0.9.8}/tests/test_policy.py +0 -0
  70. {margin-0.9.6 → margin-0.9.8}/tests/test_policy_compose.py +0 -0
  71. {margin-0.9.6 → margin-0.9.8}/tests/test_policy_temporal.py +0 -0
  72. {margin-0.9.6 → margin-0.9.8}/tests/test_policy_trace.py +0 -0
  73. {margin-0.9.6 → margin-0.9.8}/tests/test_policy_tuning.py +0 -0
  74. {margin-0.9.6 → margin-0.9.8}/tests/test_predicates.py +0 -0
  75. {margin-0.9.6 → margin-0.9.8}/tests/test_printer3d.py +0 -0
  76. {margin-0.9.6 → margin-0.9.8}/tests/test_provenance.py +0 -0
  77. {margin-0.9.6 → margin-0.9.8}/tests/test_reverse_bridge.py +0 -0
  78. {margin-0.9.6 → margin-0.9.8}/tests/test_ros2.py +0 -0
  79. {margin-0.9.6 → margin-0.9.8}/tests/test_staleness.py +0 -0
  80. {margin-0.9.6 → margin-0.9.8}/tests/test_streaming.py +0 -0
  81. {margin-0.9.6 → margin-0.9.8}/tests/test_transformer.py +0 -0
  82. {margin-0.9.6 → margin-0.9.8}/tests/test_transitions.py +0 -0
  83. {margin-0.9.6 → margin-0.9.8}/tests/test_uncertain.py +0 -0
  84. {margin-0.9.6 → margin-0.9.8}/tests/test_validity.py +0 -0
  85. {margin-0.9.6 → margin-0.9.8}/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.8
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 []
@@ -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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.9.6
3
+ Version: 0.9.8
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.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