margin 0.9.32__tar.gz → 0.10.0__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 (96) hide show
  1. {margin-0.9.32/margin.egg-info → margin-0.10.0}/PKG-INFO +42 -24
  2. {margin-0.9.32 → margin-0.10.0}/README.md +41 -23
  3. {margin-0.9.32 → margin-0.10.0}/margin/__init__.py +44 -21
  4. {margin-0.9.32 → margin-0.10.0}/margin/__main__.py +19 -0
  5. {margin-0.9.32 → margin-0.10.0}/margin/causal.py +13 -0
  6. {margin-0.9.32 → margin-0.10.0}/margin/composite.py +4 -0
  7. {margin-0.9.32 → margin-0.10.0}/margin/diff.py +54 -16
  8. {margin-0.9.32 → margin-0.10.0}/margin/loop.py +44 -0
  9. {margin-0.9.32 → margin-0.10.0}/margin/provenance.py +0 -3
  10. {margin-0.9.32 → margin-0.10.0}/margin/uncertain.py +0 -6
  11. {margin-0.9.32 → margin-0.10.0/margin.egg-info}/PKG-INFO +42 -24
  12. {margin-0.9.32 → margin-0.10.0}/margin.egg-info/SOURCES.txt +0 -5
  13. {margin-0.9.32 → margin-0.10.0}/pyproject.toml +1 -1
  14. margin-0.9.32/tests/test_healthcare.py +0 -210
  15. margin-0.9.32/tests/test_neuro.py +0 -159
  16. margin-0.9.32/tests/test_printer3d.py +0 -151
  17. margin-0.9.32/tests/test_ros2.py +0 -190
  18. margin-0.9.32/tests/test_transformer.py +0 -301
  19. {margin-0.9.32 → margin-0.10.0}/LICENSE +0 -0
  20. {margin-0.9.32 → margin-0.10.0}/NOTICE +0 -0
  21. {margin-0.9.32 → margin-0.10.0}/margin/algebra.py +0 -0
  22. {margin-0.9.32 → margin-0.10.0}/margin/anomaly.py +0 -0
  23. {margin-0.9.32 → margin-0.10.0}/margin/bridge.py +0 -0
  24. {margin-0.9.32 → margin-0.10.0}/margin/calibrate.py +0 -0
  25. {margin-0.9.32 → margin-0.10.0}/margin/confidence.py +0 -0
  26. {margin-0.9.32 → margin-0.10.0}/margin/config.py +0 -0
  27. {margin-0.9.32 → margin-0.10.0}/margin/contract.py +0 -0
  28. {margin-0.9.32 → margin-0.10.0}/margin/controller.py +0 -0
  29. {margin-0.9.32 → margin-0.10.0}/margin/correlate.py +0 -0
  30. {margin-0.9.32 → margin-0.10.0}/margin/drift.py +0 -0
  31. {margin-0.9.32 → margin-0.10.0}/margin/events.py +0 -0
  32. {margin-0.9.32 → margin-0.10.0}/margin/fingerprint.py +0 -0
  33. {margin-0.9.32 → margin-0.10.0}/margin/flow.py +0 -0
  34. {margin-0.9.32 → margin-0.10.0}/margin/forecast.py +0 -0
  35. {margin-0.9.32 → margin-0.10.0}/margin/health.py +0 -0
  36. {margin-0.9.32 → margin-0.10.0}/margin/intent.py +0 -0
  37. {margin-0.9.32 → margin-0.10.0}/margin/issue.py +0 -0
  38. {margin-0.9.32 → margin-0.10.0}/margin/ledger.py +0 -0
  39. {margin-0.9.32 → margin-0.10.0}/margin/observation.py +0 -0
  40. {margin-0.9.32 → margin-0.10.0}/margin/persist.py +0 -0
  41. {margin-0.9.32 → margin-0.10.0}/margin/policy/__init__.py +0 -0
  42. {margin-0.9.32 → margin-0.10.0}/margin/policy/compose.py +0 -0
  43. {margin-0.9.32 → margin-0.10.0}/margin/policy/core.py +0 -0
  44. {margin-0.9.32 → margin-0.10.0}/margin/policy/temporal.py +0 -0
  45. {margin-0.9.32 → margin-0.10.0}/margin/policy/trace.py +0 -0
  46. {margin-0.9.32 → margin-0.10.0}/margin/policy/tuning.py +0 -0
  47. {margin-0.9.32 → margin-0.10.0}/margin/policy/validate.py +0 -0
  48. {margin-0.9.32 → margin-0.10.0}/margin/predicates.py +0 -0
  49. {margin-0.9.32 → margin-0.10.0}/margin/streaming.py +0 -0
  50. {margin-0.9.32 → margin-0.10.0}/margin/transitions.py +0 -0
  51. {margin-0.9.32 → margin-0.10.0}/margin/validity.py +0 -0
  52. {margin-0.9.32 → margin-0.10.0}/margin.egg-info/dependency_links.txt +0 -0
  53. {margin-0.9.32 → margin-0.10.0}/margin.egg-info/top_level.txt +0 -0
  54. {margin-0.9.32 → margin-0.10.0}/setup.cfg +0 -0
  55. {margin-0.9.32 → margin-0.10.0}/tests/test_absence.py +0 -0
  56. {margin-0.9.32 → margin-0.10.0}/tests/test_algebra.py +0 -0
  57. {margin-0.9.32 → margin-0.10.0}/tests/test_anomaly.py +0 -0
  58. {margin-0.9.32 → margin-0.10.0}/tests/test_bridge.py +0 -0
  59. {margin-0.9.32 → margin-0.10.0}/tests/test_calibrate.py +0 -0
  60. {margin-0.9.32 → margin-0.10.0}/tests/test_causal.py +0 -0
  61. {margin-0.9.32 → margin-0.10.0}/tests/test_composite.py +0 -0
  62. {margin-0.9.32 → margin-0.10.0}/tests/test_confidence.py +0 -0
  63. {margin-0.9.32 → margin-0.10.0}/tests/test_config.py +0 -0
  64. {margin-0.9.32 → margin-0.10.0}/tests/test_contract.py +0 -0
  65. {margin-0.9.32 → margin-0.10.0}/tests/test_controller.py +0 -0
  66. {margin-0.9.32 → margin-0.10.0}/tests/test_correlate.py +0 -0
  67. {margin-0.9.32 → margin-0.10.0}/tests/test_diff.py +0 -0
  68. {margin-0.9.32 → margin-0.10.0}/tests/test_drift.py +0 -0
  69. {margin-0.9.32 → margin-0.10.0}/tests/test_events.py +0 -0
  70. {margin-0.9.32 → margin-0.10.0}/tests/test_fingerprint_ext.py +0 -0
  71. {margin-0.9.32 → margin-0.10.0}/tests/test_float_tolerance.py +0 -0
  72. {margin-0.9.32 → margin-0.10.0}/tests/test_flow.py +0 -0
  73. {margin-0.9.32 → margin-0.10.0}/tests/test_forecast.py +0 -0
  74. {margin-0.9.32 → margin-0.10.0}/tests/test_health.py +0 -0
  75. {margin-0.9.32 → margin-0.10.0}/tests/test_intent.py +0 -0
  76. {margin-0.9.32 → margin-0.10.0}/tests/test_issue.py +0 -0
  77. {margin-0.9.32 → margin-0.10.0}/tests/test_ledger.py +0 -0
  78. {margin-0.9.32 → margin-0.10.0}/tests/test_loop.py +0 -0
  79. {margin-0.9.32 → margin-0.10.0}/tests/test_observation.py +0 -0
  80. {margin-0.9.32 → margin-0.10.0}/tests/test_persist.py +0 -0
  81. {margin-0.9.32 → margin-0.10.0}/tests/test_pipeline_features.py +0 -0
  82. {margin-0.9.32 → margin-0.10.0}/tests/test_policy.py +0 -0
  83. {margin-0.9.32 → margin-0.10.0}/tests/test_policy_compose.py +0 -0
  84. {margin-0.9.32 → margin-0.10.0}/tests/test_policy_temporal.py +0 -0
  85. {margin-0.9.32 → margin-0.10.0}/tests/test_policy_trace.py +0 -0
  86. {margin-0.9.32 → margin-0.10.0}/tests/test_policy_tuning.py +0 -0
  87. {margin-0.9.32 → margin-0.10.0}/tests/test_policy_validate.py +0 -0
  88. {margin-0.9.32 → margin-0.10.0}/tests/test_predicates.py +0 -0
  89. {margin-0.9.32 → margin-0.10.0}/tests/test_provenance.py +0 -0
  90. {margin-0.9.32 → margin-0.10.0}/tests/test_reverse_bridge.py +0 -0
  91. {margin-0.9.32 → margin-0.10.0}/tests/test_staleness.py +0 -0
  92. {margin-0.9.32 → margin-0.10.0}/tests/test_streaming.py +0 -0
  93. {margin-0.9.32 → margin-0.10.0}/tests/test_transitions.py +0 -0
  94. {margin-0.9.32 → margin-0.10.0}/tests/test_uncertain.py +0 -0
  95. {margin-0.9.32 → margin-0.10.0}/tests/test_validity.py +0 -0
  96. {margin-0.9.32 → margin-0.10.0}/tests/test_windowing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.9.32
3
+ Version: 0.10.0
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
@@ -34,9 +34,35 @@ Dynamic: license-file
34
34
  [![Downloads/month](https://static.pepy.tech/badge/margin/month)](https://pepy.tech/project/margin)
35
35
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
36
36
 
37
- **Typed health classification for systems that measure things.**
37
+ **Typed primitives for system self-awareness.**
38
38
 
39
- Every system with health bars, thresholds, alerts, or status dashboards solves the same problem: take a number, decide if it's healthy, correct it if it isn't, explain what happened. Margin is that pattern, typed once, with the polarity bug fixed.
39
+ When a system has to answer *"how am I doing, and what should I do about it?"* — an LLM agent between steps, an ML pipeline mid-inference, a data job deciding whether to ship — margin gives it the vocabulary. Typed `Health`, `Confidence`, `Absence`, `Issue`, `Flow`, `Intent`, `Contract` — composable, serializable, zero-dependency Python.
40
+
41
+ ```python
42
+ from adapters.agent import agent_parser, AgentStep, agent_flow
43
+
44
+ parser = agent_parser()
45
+ flow = agent_flow() # plan → act → reflect
46
+
47
+ # Record what you already measure after one LLM + tool step:
48
+ step = AgentStep(latency_ms=2400, tokens_used=1800,
49
+ tool_success_rate=0.9, retry_count=1, confidence=0.72)
50
+
51
+ expr = parser.parse(step.to_values())
52
+ guidance = flow.observe(step.to_values())
53
+
54
+ print(expr.to_string())
55
+ # [latency_ms:INTACT(-0.60σ)] [tokens_used:INTACT(-1.25σ)]
56
+ # [tool_success_rate:INTACT(-0.05σ)] [retry_count:INTACT(+0.00σ)]
57
+ # [confidence:INTACT(-0.10σ)]
58
+
59
+ print(guidance.to_atom())
60
+ # flow:agent:plan[1/3] → ADVANCE
61
+ ```
62
+
63
+ Every metric is polarity-aware (more tokens = worse, higher confidence = better), sigma-normalised so you can compare them, and emits typed diagnostics when something goes wrong. Nothing stringly-typed; your decision loop reads typed enums.
64
+
65
+ Not building an agent? Margin works on any scalar with a threshold:
40
66
 
41
67
  ```python
42
68
  from margin import Parser, Thresholds
@@ -48,13 +74,12 @@ parser = Parser(
48
74
  "error_rate": Thresholds(intact=0.005, ablated=0.05, higher_is_better=False),
49
75
  },
50
76
  )
51
-
52
77
  expr = parser.parse({"throughput": 480.0, "error_rate": 0.03})
53
78
  print(expr.to_string())
54
79
  # [throughput:INTACT(-0.04σ)] [error_rate:DEGRADED(-14.00σ)]
55
80
  ```
56
81
 
57
- Throughput and error rate on the same scale. One is higher-is-better, the other is lower-is-better. Both classified correctly. Sigma-normalised so you can compare them.
82
+ Throughput and error rate on the same scale. One higher-is-better, the other lower-is-better. Both classified correctly. Sigma-normalised so you can compare them.
58
83
 
59
84
  ## Install
60
85
 
@@ -203,30 +228,23 @@ parser = parser_from_calibration(
203
228
 
204
229
  `full_step()` orchestrates the first seven in one call. `Flow` composes them into a sequence. `Issue` is ambient — every layer emits diagnostics into the same buffer.
205
230
 
206
- ## Domain adapters
231
+ ## Adapters
207
232
 
208
- Ready-to-use threshold profiles for specific domains:
233
+ Core adapters ship in the package. Each is a Parser factory + optional Flow / Intent / Monitor helpers with domain-appropriate thresholds:
209
234
 
210
- | Adapter | What it monitors | Polarity |
211
- |---|---|---|
212
- | [**healthcare**](https://github.com/Cope-Labs/margin/tree/main/adapters/healthcare/) | Vital signs (HR, BP, SpO2, temp, glucose) — WHO/AHA ranges, sepsis screening | bands |
213
- | [**godot**](https://github.com/sethc5/margin-godot) | Game systems (food, morale, stress, stamina) — native GDScript | mixed |
214
- | [**homeassistant**](https://github.com/Cope-Labs/margin/tree/main/adapters/homeassistant/) | Smart home sensors (temp, humidity, battery, solar, power) | mixed |
215
- | [**evcharging**](https://github.com/Cope-Labs/margin/tree/main/adapters/evcharging/) | EV charging (SoC, grid draw, solar surplus, efficiency) | mixed |
216
- | [**infrastructure**](https://github.com/Cope-Labs/margin/tree/main/adapters/infrastructure/) | Server monitoring (CPU, memory, disk, latency, error rate) | mixed |
217
- | [**aquarium**](https://github.com/Cope-Labs/margin/tree/main/adapters/aquarium/) | Water chemistry (pH, ammonia, nitrite, temp, hardness) | bands |
218
- | [**greenhouse**](https://github.com/Cope-Labs/margin/tree/main/adapters/greenhouse/) | Growing environment (soil moisture, CO₂, light, VPD) | bands |
219
- | [**fitness**](https://github.com/Cope-Labs/margin/tree/main/adapters/fitness/) | Wearables (resting HR, HRV, sleep, steps, stress) | mixed |
220
- | [**transformer**](https://github.com/Cope-Labs/margin/tree/main/adapters/transformer/) | ML circuit interpretability (pythia-6.9b) | higher |
221
- | [**fastapi**](https://github.com/Cope-Labs/margin/tree/main/adapters/fastapi/) | Endpoint health (latency, error rate, throughput, queue depth) | mixed |
222
- | [**database**](https://github.com/Cope-Labs/margin/tree/main/adapters/database/) | DB health (pool usage, query latency, replication lag, deadlocks) | mixed |
223
- | [**celery**](https://github.com/Cope-Labs/margin/tree/main/adapters/celery/) | Task queue (queue depth, failure rate, worker utilization, retries) | mixed |
224
- | [**dataframe**](https://github.com/Cope-Labs/margin/tree/main/adapters/dataframe/) | Data quality (completeness, null rate, drift, freshness, schema) | mixed |
225
- | [**pytest**](https://github.com/Cope-Labs/margin/tree/main/adapters/pytest/) | Test suite health (pass rate, flake rate, coverage, duration) | mixed |
235
+ | Adapter | What it monitors |
236
+ |---|---|
237
+ | [**agent**](https://github.com/Cope-Labs/margin/tree/main/adapters/agent/) | LLM / agent step health — latency, tokens, tool success rate, retries, confidence. Includes a three-stage `plan → act → reflect` Flow and a default feasibility Intent. |
238
+ | [**dataframe**](https://github.com/Cope-Labs/margin/tree/main/adapters/dataframe/) | Data quality (completeness, null rate, drift, freshness, schema) |
239
+ | [**numpy**](https://github.com/Cope-Labs/margin/tree/main/adapters/numpy/) | Array health (NaN rate, drift, distribution shape) |
240
+ | [**fastapi**](https://github.com/Cope-Labs/margin/tree/main/adapters/fastapi/) | Endpoint health (latency, error rate, throughput, queue depth) — ASGI middleware |
241
+ | [**pytest**](https://github.com/Cope-Labs/margin/tree/main/adapters/pytest/) | Test suite health (pass rate, flake rate, coverage, duration) — conftest plugin |
242
+
243
+ Additional domain profiles (healthcare, ros2, transformer circuit interpretability, homeassistant, aquarium, greenhouse, evcharging, fitness, infrastructure, weather, printer3d, neuro, godot, celery, database) live in [`contrib/`](https://github.com/Cope-Labs/margin/tree/main/contrib/) — reference material, not shipped in the pip package.
226
244
 
227
245
  ## Docs
228
246
 
229
- Full specification: [margin-language.md](https://github.com/Cope-Labs/margin/blob/main/margin/margin-language.md)
247
+ Full specification: [margin-language.md](https://github.com/Cope-Labs/margin/blob/main/margin/margin-language.md) — a typed grammar for system self-awareness.
230
248
 
231
249
  ## License
232
250
 
@@ -5,9 +5,35 @@
5
5
  [![Downloads/month](https://static.pepy.tech/badge/margin/month)](https://pepy.tech/project/margin)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
- **Typed health classification for systems that measure things.**
8
+ **Typed primitives for system self-awareness.**
9
9
 
10
- Every system with health bars, thresholds, alerts, or status dashboards solves the same problem: take a number, decide if it's healthy, correct it if it isn't, explain what happened. Margin is that pattern, typed once, with the polarity bug fixed.
10
+ When a system has to answer *"how am I doing, and what should I do about it?"* — an LLM agent between steps, an ML pipeline mid-inference, a data job deciding whether to ship — margin gives it the vocabulary. Typed `Health`, `Confidence`, `Absence`, `Issue`, `Flow`, `Intent`, `Contract` — composable, serializable, zero-dependency Python.
11
+
12
+ ```python
13
+ from adapters.agent import agent_parser, AgentStep, agent_flow
14
+
15
+ parser = agent_parser()
16
+ flow = agent_flow() # plan → act → reflect
17
+
18
+ # Record what you already measure after one LLM + tool step:
19
+ step = AgentStep(latency_ms=2400, tokens_used=1800,
20
+ tool_success_rate=0.9, retry_count=1, confidence=0.72)
21
+
22
+ expr = parser.parse(step.to_values())
23
+ guidance = flow.observe(step.to_values())
24
+
25
+ print(expr.to_string())
26
+ # [latency_ms:INTACT(-0.60σ)] [tokens_used:INTACT(-1.25σ)]
27
+ # [tool_success_rate:INTACT(-0.05σ)] [retry_count:INTACT(+0.00σ)]
28
+ # [confidence:INTACT(-0.10σ)]
29
+
30
+ print(guidance.to_atom())
31
+ # flow:agent:plan[1/3] → ADVANCE
32
+ ```
33
+
34
+ Every metric is polarity-aware (more tokens = worse, higher confidence = better), sigma-normalised so you can compare them, and emits typed diagnostics when something goes wrong. Nothing stringly-typed; your decision loop reads typed enums.
35
+
36
+ Not building an agent? Margin works on any scalar with a threshold:
11
37
 
12
38
  ```python
13
39
  from margin import Parser, Thresholds
@@ -19,13 +45,12 @@ parser = Parser(
19
45
  "error_rate": Thresholds(intact=0.005, ablated=0.05, higher_is_better=False),
20
46
  },
21
47
  )
22
-
23
48
  expr = parser.parse({"throughput": 480.0, "error_rate": 0.03})
24
49
  print(expr.to_string())
25
50
  # [throughput:INTACT(-0.04σ)] [error_rate:DEGRADED(-14.00σ)]
26
51
  ```
27
52
 
28
- Throughput and error rate on the same scale. One is higher-is-better, the other is lower-is-better. Both classified correctly. Sigma-normalised so you can compare them.
53
+ Throughput and error rate on the same scale. One higher-is-better, the other lower-is-better. Both classified correctly. Sigma-normalised so you can compare them.
29
54
 
30
55
  ## Install
31
56
 
@@ -174,30 +199,23 @@ parser = parser_from_calibration(
174
199
 
175
200
  `full_step()` orchestrates the first seven in one call. `Flow` composes them into a sequence. `Issue` is ambient — every layer emits diagnostics into the same buffer.
176
201
 
177
- ## Domain adapters
202
+ ## Adapters
178
203
 
179
- Ready-to-use threshold profiles for specific domains:
204
+ Core adapters ship in the package. Each is a Parser factory + optional Flow / Intent / Monitor helpers with domain-appropriate thresholds:
180
205
 
181
- | Adapter | What it monitors | Polarity |
182
- |---|---|---|
183
- | [**healthcare**](https://github.com/Cope-Labs/margin/tree/main/adapters/healthcare/) | Vital signs (HR, BP, SpO2, temp, glucose) — WHO/AHA ranges, sepsis screening | bands |
184
- | [**godot**](https://github.com/sethc5/margin-godot) | Game systems (food, morale, stress, stamina) — native GDScript | mixed |
185
- | [**homeassistant**](https://github.com/Cope-Labs/margin/tree/main/adapters/homeassistant/) | Smart home sensors (temp, humidity, battery, solar, power) | mixed |
186
- | [**evcharging**](https://github.com/Cope-Labs/margin/tree/main/adapters/evcharging/) | EV charging (SoC, grid draw, solar surplus, efficiency) | mixed |
187
- | [**infrastructure**](https://github.com/Cope-Labs/margin/tree/main/adapters/infrastructure/) | Server monitoring (CPU, memory, disk, latency, error rate) | mixed |
188
- | [**aquarium**](https://github.com/Cope-Labs/margin/tree/main/adapters/aquarium/) | Water chemistry (pH, ammonia, nitrite, temp, hardness) | bands |
189
- | [**greenhouse**](https://github.com/Cope-Labs/margin/tree/main/adapters/greenhouse/) | Growing environment (soil moisture, CO₂, light, VPD) | bands |
190
- | [**fitness**](https://github.com/Cope-Labs/margin/tree/main/adapters/fitness/) | Wearables (resting HR, HRV, sleep, steps, stress) | mixed |
191
- | [**transformer**](https://github.com/Cope-Labs/margin/tree/main/adapters/transformer/) | ML circuit interpretability (pythia-6.9b) | higher |
192
- | [**fastapi**](https://github.com/Cope-Labs/margin/tree/main/adapters/fastapi/) | Endpoint health (latency, error rate, throughput, queue depth) | mixed |
193
- | [**database**](https://github.com/Cope-Labs/margin/tree/main/adapters/database/) | DB health (pool usage, query latency, replication lag, deadlocks) | mixed |
194
- | [**celery**](https://github.com/Cope-Labs/margin/tree/main/adapters/celery/) | Task queue (queue depth, failure rate, worker utilization, retries) | mixed |
195
- | [**dataframe**](https://github.com/Cope-Labs/margin/tree/main/adapters/dataframe/) | Data quality (completeness, null rate, drift, freshness, schema) | mixed |
196
- | [**pytest**](https://github.com/Cope-Labs/margin/tree/main/adapters/pytest/) | Test suite health (pass rate, flake rate, coverage, duration) | mixed |
206
+ | Adapter | What it monitors |
207
+ |---|---|
208
+ | [**agent**](https://github.com/Cope-Labs/margin/tree/main/adapters/agent/) | LLM / agent step health — latency, tokens, tool success rate, retries, confidence. Includes a three-stage `plan → act → reflect` Flow and a default feasibility Intent. |
209
+ | [**dataframe**](https://github.com/Cope-Labs/margin/tree/main/adapters/dataframe/) | Data quality (completeness, null rate, drift, freshness, schema) |
210
+ | [**numpy**](https://github.com/Cope-Labs/margin/tree/main/adapters/numpy/) | Array health (NaN rate, drift, distribution shape) |
211
+ | [**fastapi**](https://github.com/Cope-Labs/margin/tree/main/adapters/fastapi/) | Endpoint health (latency, error rate, throughput, queue depth) — ASGI middleware |
212
+ | [**pytest**](https://github.com/Cope-Labs/margin/tree/main/adapters/pytest/) | Test suite health (pass rate, flake rate, coverage, duration) — conftest plugin |
213
+
214
+ Additional domain profiles (healthcare, ros2, transformer circuit interpretability, homeassistant, aquarium, greenhouse, evcharging, fitness, infrastructure, weather, printer3d, neuro, godot, celery, database) live in [`contrib/`](https://github.com/Cope-Labs/margin/tree/main/contrib/) — reference material, not shipped in the pip package.
197
215
 
198
216
  ## Docs
199
217
 
200
- Full specification: [margin-language.md](https://github.com/Cope-Labs/margin/blob/main/margin/margin-language.md)
218
+ Full specification: [margin-language.md](https://github.com/Cope-Labs/margin/blob/main/margin/margin-language.md) — a typed grammar for system self-awareness.
201
219
 
202
220
  ## License
203
221
 
@@ -18,10 +18,10 @@ Structure:
18
18
  from .confidence import Confidence
19
19
  from .validity import Validity, ValidityMode
20
20
  from .provenance import (
21
- new_id, _new_id, are_correlated, merge, merge_provenance,
21
+ new_id, _new_id, are_correlated, merge,
22
22
  ProvenanceNode, ProvenanceGraph, create_root_provenance,
23
23
  )
24
- from .uncertain import UncertainValue, Source, EpistemicSource, UncertaintyMode
24
+ from .uncertain import UncertainValue, Source
25
25
  from .algebra import add, subtract, multiply, divide, scale, compare, weighted_average
26
26
  from .health import Health, Thresholds, classify, SEVERITY
27
27
  from .observation import Absence, Op, Observation, Correction, Expression, Parser
@@ -200,24 +200,47 @@ __all__ = [
200
200
  "Issue", "IssueLevel", "IssueCode", "IssueBuffer", "ISSUE_SEVERITY",
201
201
  # Flow
202
202
  "Flow", "Stage", "Guidance", "FlowResult", "GUIDANCE_SEVERITY",
203
- # margin-poc compat aliases
204
- "EpistemicSource", "UncertaintyMode",
205
- "CallConfidence", "CircuitHealth", "GapThresholds", "MarginParser",
206
- "CircuitUtterance", "CorrectionOp", "MarginExpression",
207
- "CorrectionRequest", "CorrectionRecord", "CorrectionLedger",
208
- "classify_gap_health", "compare_to_threshold", "merge_provenance",
209
203
  ]
210
204
 
211
- # margin-poc backward-compatible type aliases
212
- CallConfidence = Confidence
213
- CircuitHealth = Health
214
- GapThresholds = Thresholds
215
- MarginParser = Parser
216
- CircuitUtterance = Observation
217
- CorrectionOp = Op
218
- MarginExpression = Expression
219
- CorrectionRequest = Correction
220
- CorrectionRecord = Record
221
- CorrectionLedger = Ledger
222
- classify_gap_health = classify
223
- compare_to_threshold = compare
205
+ # -----------------------------------------------------------------------
206
+ # margin-poc backward-compat aliases
207
+ #
208
+ # These names come from the pre-margin proof-of-concept and are kept
209
+ # importable so downstream code doesn't break on upgrade. They are
210
+ # NOT part of the public surface (excluded from __all__) and emit a
211
+ # DeprecationWarning on access. Scheduled for removal at 1.0.
212
+ # -----------------------------------------------------------------------
213
+
214
+ _DEPRECATED_ALIASES: dict[str, str] = {
215
+ "CallConfidence": "Confidence",
216
+ "CircuitHealth": "Health",
217
+ "GapThresholds": "Thresholds",
218
+ "MarginParser": "Parser",
219
+ "CircuitUtterance": "Observation",
220
+ "CorrectionOp": "Op",
221
+ "MarginExpression": "Expression",
222
+ "CorrectionRequest": "Correction",
223
+ "CorrectionRecord": "Record",
224
+ "CorrectionLedger": "Ledger",
225
+ "classify_gap_health": "classify",
226
+ "compare_to_threshold": "compare",
227
+ "merge_provenance": "merge",
228
+ "EpistemicSource": "Source",
229
+ "UncertaintyMode": "UncertaintyMode", # no canonical replacement; plain stub
230
+ }
231
+
232
+
233
+ def __getattr__(name: str):
234
+ import warnings as _warnings
235
+ canonical = _DEPRECATED_ALIASES.get(name)
236
+ if canonical is None:
237
+ raise AttributeError(f"module 'margin' has no attribute {name!r}")
238
+ _warnings.warn(
239
+ f"margin.{name} is a deprecated margin-poc alias and will be removed in 1.0; "
240
+ f"use margin.{canonical} instead.",
241
+ DeprecationWarning,
242
+ stacklevel=2,
243
+ )
244
+ if name == "UncertaintyMode":
245
+ return type("UncertaintyMode", (), {"ABSOLUTE": "absolute", "RELATIVE": "relative"})()
246
+ return globals()[canonical]
@@ -47,6 +47,9 @@ def cmd_monitor(args):
47
47
  except ValueError:
48
48
  pass
49
49
  if not values:
50
+ preview = line[:60] + ("…" if len(line) > 60 else "")
51
+ print(f"margin monitor: skipping unparseable line: {preview!r}",
52
+ file=sys.stderr)
50
53
  continue
51
54
 
52
55
  expr = monitor.update(values, now=datetime.now())
@@ -69,6 +72,12 @@ def cmd_monitor(args):
69
72
  print("\n".join(parts))
70
73
  sys.stdout.flush()
71
74
 
75
+ # Surface any ERROR/FATAL issues emitted during this update to stderr.
76
+ for issue in expr.issues:
77
+ if issue.level.value in ("ERROR", "FATAL"):
78
+ print(f" ! {issue.to_atom()} {issue.detail}", file=sys.stderr)
79
+ sys.stderr.flush()
80
+
72
81
 
73
82
  def cmd_replay(args):
74
83
  """Replay a CSV through Monitor and print analysis."""
@@ -119,6 +128,14 @@ def cmd_replay(args):
119
128
  for c in monitor.correlations.strongest(5):
120
129
  print(f" {c.component_a} ~ {c.component_b}: r={c.coefficient:.3f}")
121
130
 
131
+ # Issue summary from the monitor's buffer
132
+ if getattr(monitor, "issues", None) and len(monitor.issues) > 0:
133
+ counts = monitor.issues.counts()
134
+ non_zero = {k: v for k, v in counts.items() if v > 0}
135
+ if non_zero:
136
+ print()
137
+ print("Issues:", ", ".join(f"{k}={v}" for k, v in non_zero.items()))
138
+
122
139
  # Write full output if requested
123
140
  if args.output:
124
141
  Path(args.output).write_text(json.dumps(snapshots, indent=2))
@@ -148,6 +165,8 @@ def cmd_status(args):
148
165
 
149
166
  expr = parser.parse(values)
150
167
  print(expr.to_string())
168
+ for issue in expr.issues:
169
+ print(f" {issue.to_atom()} {issue.detail}", file=sys.stderr)
151
170
 
152
171
 
153
172
  def main():
@@ -314,6 +314,19 @@ class Explanation:
314
314
  cause_strs = [c.to_string() for c in self.causes]
315
315
  return f"{self.component}:{h} ← " + ", ".join(cause_strs)
316
316
 
317
+ def to_atom(self) -> str:
318
+ """Compact single-line form for log aggregation.
319
+
320
+ ``component:HEALTH`` when there's no known cause,
321
+ ``component:HEALTH ← source:HEALTH`` otherwise (strongest cause only).
322
+ """
323
+ h = self.health.value if self.health else "?"
324
+ rc = self.root_cause
325
+ if rc is None:
326
+ return f"{self.component}:{h}"
327
+ src_h = rc.source_health.value if rc.source_health else "?"
328
+ return f"{self.component}:{h} ← {rc.source}:{src_h}"
329
+
317
330
  def to_dict(self) -> dict:
318
331
  return {
319
332
  "component": self.component,
@@ -93,6 +93,8 @@ class CompositeObservation:
93
93
  """
94
94
  Flatten to a single Observation using the aggregate health.
95
95
  Uses the worst sub-observation's value/baseline for sigma.
96
+ If the worst sub-observation carries an Absence, it propagates to the
97
+ flattened Observation so downstream consumers don't silently lose the reason.
96
98
  """
97
99
  w = self.worst
98
100
  if w is None:
@@ -110,6 +112,8 @@ class CompositeObservation:
110
112
  higher_is_better=w.higher_is_better,
111
113
  provenance=w.provenance,
112
114
  measured_at=self.measured_at,
115
+ absence=w.absence,
116
+ absence_detail=w.absence_detail,
113
117
  )
114
118
 
115
119
  def to_atom(self) -> str:
@@ -8,7 +8,7 @@ from dataclasses import dataclass, field
8
8
  from typing import Optional
9
9
 
10
10
  from .health import Health, SEVERITY
11
- from .observation import Observation, Expression
11
+ from .observation import Absence, Observation, Expression
12
12
  from .confidence import Confidence
13
13
 
14
14
 
@@ -17,22 +17,38 @@ class ComponentChange:
17
17
  """
18
18
  What changed for a single component between two expressions.
19
19
 
20
- name: component identifier
21
- health_before: Health in the earlier expression (None if component was absent)
22
- health_after: Health in the later expression (None if component disappeared)
23
- sigma_before: sigma in the earlier expression
24
- sigma_after: sigma in the later expression
20
+ name: component identifier
21
+ health_before: Health in the earlier expression (None if component was missing)
22
+ health_after: Health in the later expression (None if component disappeared)
23
+ sigma_before: sigma in the earlier expression
24
+ sigma_after: sigma in the later expression
25
+ absence_before: Absence reason in the earlier expression (None if present)
26
+ absence_after: Absence reason in the later expression (None if present)
25
27
  """
26
28
  name: str
27
29
  health_before: Optional[Health]
28
30
  health_after: Optional[Health]
29
31
  sigma_before: Optional[float]
30
32
  sigma_after: Optional[float]
33
+ absence_before: Optional[Absence] = None
34
+ absence_after: Optional[Absence] = None
31
35
 
32
36
  @property
33
37
  def health_changed(self) -> bool:
34
38
  return self.health_before != self.health_after
35
39
 
40
+ @property
41
+ def absence_changed(self) -> bool:
42
+ return self.absence_before != self.absence_after
43
+
44
+ @property
45
+ def became_absent(self) -> bool:
46
+ return self.absence_before is None and self.absence_after is not None
47
+
48
+ @property
49
+ def became_present(self) -> bool:
50
+ return self.absence_before is not None and self.absence_after is None
51
+
36
52
  @property
37
53
  def sigma_delta(self) -> Optional[float]:
38
54
  """Change in sigma. Positive = healthier. None if either side missing."""
@@ -64,19 +80,26 @@ class ComponentChange:
64
80
  return False
65
81
  return SEVERITY.get(self.health_after, 0) < SEVERITY.get(self.health_before, 0)
66
82
 
83
+ def _side(self, health: Optional[Health], absence: Optional[Absence]) -> str:
84
+ if absence is not None:
85
+ return f"ABSENT({absence.value})"
86
+ if health is not None:
87
+ return health.value
88
+ return "missing"
89
+
67
90
  def to_string(self) -> str:
68
- b = self.health_before.value if self.health_before else "absent"
69
- a = self.health_after.value if self.health_after else "absent"
70
- if not self.health_changed:
71
- sd = self.sigma_delta
72
- if sd is not None and abs(sd) > 0.001:
73
- sign = "+" if sd >= 0 else ""
74
- return f"{self.name}: {a} ({sign}{sd:.2f}σ)"
75
- return f"{self.name}: {a} (unchanged)"
76
- return f"{self.name}: {b} → {a}"
91
+ b = self._side(self.health_before, self.absence_before)
92
+ a = self._side(self.health_after, self.absence_after)
93
+ if self.health_changed or self.absence_changed:
94
+ return f"{self.name}: {b} → {a}"
95
+ sd = self.sigma_delta
96
+ if sd is not None and abs(sd) > 0.001:
97
+ sign = "+" if sd >= 0 else ""
98
+ return f"{self.name}: {a} ({sign}{sd:.2f}σ)"
99
+ return f"{self.name}: {a} (unchanged)"
77
100
 
78
101
  def to_dict(self) -> dict:
79
- return {
102
+ d = {
80
103
  "name": self.name,
81
104
  "health_before": self.health_before.value if self.health_before else None,
82
105
  "health_after": self.health_after.value if self.health_after else None,
@@ -87,6 +110,13 @@ class ComponentChange:
87
110
  "worsened": self.worsened,
88
111
  "improved": self.improved,
89
112
  }
113
+ if self.absence_before is not None or self.absence_after is not None:
114
+ d["absence_before"] = self.absence_before.value if self.absence_before else None
115
+ d["absence_after"] = self.absence_after.value if self.absence_after else None
116
+ d["absence_changed"] = self.absence_changed
117
+ d["became_absent"] = self.became_absent
118
+ d["became_present"] = self.became_present
119
+ return d
90
120
 
91
121
 
92
122
 
@@ -127,6 +157,12 @@ class Diff:
127
157
  def disappeared(self) -> list[ComponentChange]:
128
158
  return [c for c in self.changes if c.disappeared]
129
159
 
160
+ def became_absent(self) -> list[ComponentChange]:
161
+ return [c for c in self.changes if c.became_absent]
162
+
163
+ def became_present(self) -> list[ComponentChange]:
164
+ return [c for c in self.changes if c.became_present]
165
+
130
166
  def to_string(self) -> str:
131
167
  if not self.changes:
132
168
  return "(no components)"
@@ -169,6 +205,8 @@ def diff(before: Expression, after: Expression) -> Diff:
169
205
  health_after=a.health if a else None,
170
206
  sigma_before=b.sigma if b else None,
171
207
  sigma_after=a.sigma if a else None,
208
+ absence_before=b.absence if b else None,
209
+ absence_after=a.absence if a else None,
172
210
  ))
173
211
 
174
212
  return Diff(
@@ -106,6 +106,37 @@ class StepResult:
106
106
  lines.append(f" Contract: {met} met, {violated} violated, {pending} pending")
107
107
  return "\n".join(lines)
108
108
 
109
+ def to_atom(self) -> str:
110
+ """Compact single-line form, consistent with Observation/FlowResult atoms.
111
+
112
+ ``step[ACTION](n/m) → OP(target, α=0.3)`` when a correction fires,
113
+ ``step[NOOP](n/m)`` otherwise; optional ``, contract:MET|VIOLATED|OPEN`` tail.
114
+ """
115
+ if self.escalated:
116
+ action = "ESCALATE"
117
+ elif self.acted:
118
+ action = "CORRECT"
119
+ else:
120
+ action = "NOOP"
121
+ if self.decision is not None:
122
+ rules = f"({self.decision.rules_matched}/{self.decision.rules_considered})"
123
+ else:
124
+ rules = ""
125
+ parts = [f"step[{action}]{rules}"]
126
+ if self.correction and self.correction.is_active():
127
+ parts.append(
128
+ f"→ {self.correction.op.value}({self.correction.target}, "
129
+ f"α={self.correction.alpha:.2f})"
130
+ )
131
+ elif self.escalation:
132
+ parts.append(f"→ {self.escalation.level.value}")
133
+ if self.contract is not None:
134
+ state = "MET" if self.contract.all_met else (
135
+ "VIOLATED" if self.contract.violated() else "OPEN"
136
+ )
137
+ parts.append(f"contract:{state}")
138
+ return " ".join(parts)
139
+
109
140
  def __repr__(self) -> str:
110
141
  action = "CORRECT" if self.acted else ("ESCALATE" if self.escalated else "NOOP")
111
142
  return f"StepResult({action}, contract={'MET' if self.contract_met else 'OPEN'})"
@@ -268,6 +299,19 @@ class FullStepResult:
268
299
  lines.append(f" Intent: {self.intent.summary()}")
269
300
  return "\n".join(lines)
270
301
 
302
+ def to_atom(self) -> str:
303
+ """Compact single-line form: step atom plus non-quiescent drift/anomaly counts and intent."""
304
+ parts = [self.step.to_atom()]
305
+ drifting = sum(1 for dc in self.drift.values() if dc.state.value != "STABLE")
306
+ if drifting:
307
+ parts.append(f"drift:{drifting}")
308
+ anomalous = sum(1 for ac in self.anomaly.values() if ac.state.value != "EXPECTED")
309
+ if anomalous:
310
+ parts.append(f"anomaly:{anomalous}")
311
+ if self.intent is not None:
312
+ parts.append(f"intent:{self.intent.feasibility.value}")
313
+ return " ".join(parts)
314
+
271
315
  def __repr__(self) -> str:
272
316
  action = "CORRECT" if self.acted else "NOOP"
273
317
  intent_str = f", intent={self.intent.feasibility.value}" if self.intent else ""
@@ -28,9 +28,6 @@ def merge(ids_a: list[str], ids_b: list[str]) -> list[str]:
28
28
  """Merge two provenance chains, adding a new derived ID."""
29
29
  return list(set(ids_a) | set(ids_b) | {new_id()})
30
30
 
31
- # margin-poc compat alias
32
- merge_provenance = merge
33
-
34
31
 
35
32
  # -----------------------------------------------------------------------
36
33
  # Provenance graph — full lineage tracking
@@ -16,12 +16,6 @@ class Source:
16
16
  MODELED = "modeled" # derived from a formal model
17
17
  ASSERTED = "asserted" # declared by the caller
18
18
  PROPAGATED = "propagated" # derived by the algebra
19
- COMPUTED = "modeled" # alias for margin-poc compat (EpistemicSource.COMPUTED)
20
- INFERRED = "modeled" # alias
21
-
22
- # margin-poc compat aliases
23
- EpistemicSource = Source
24
- UncertaintyMode = type('UncertaintyMode', (), {'ABSOLUTE': 'absolute', 'RELATIVE': 'relative'})()
25
19
 
26
20
 
27
21
  class UncertainValue: