margin 0.9.31__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.31/margin.egg-info → margin-0.10.0}/PKG-INFO +96 -25
  2. {margin-0.9.31 → margin-0.10.0}/README.md +95 -24
  3. {margin-0.9.31 → margin-0.10.0}/margin/__init__.py +44 -21
  4. {margin-0.9.31 → margin-0.10.0}/margin/__main__.py +19 -0
  5. {margin-0.9.31 → margin-0.10.0}/margin/causal.py +13 -0
  6. {margin-0.9.31 → margin-0.10.0}/margin/composite.py +4 -0
  7. {margin-0.9.31 → margin-0.10.0}/margin/diff.py +54 -16
  8. {margin-0.9.31 → margin-0.10.0}/margin/loop.py +44 -0
  9. {margin-0.9.31 → margin-0.10.0}/margin/provenance.py +0 -3
  10. {margin-0.9.31 → margin-0.10.0}/margin/uncertain.py +0 -6
  11. {margin-0.9.31 → margin-0.10.0/margin.egg-info}/PKG-INFO +96 -25
  12. {margin-0.9.31 → margin-0.10.0}/margin.egg-info/SOURCES.txt +0 -5
  13. {margin-0.9.31 → margin-0.10.0}/pyproject.toml +1 -1
  14. margin-0.9.31/tests/test_healthcare.py +0 -210
  15. margin-0.9.31/tests/test_neuro.py +0 -159
  16. margin-0.9.31/tests/test_printer3d.py +0 -151
  17. margin-0.9.31/tests/test_ros2.py +0 -190
  18. margin-0.9.31/tests/test_transformer.py +0 -301
  19. {margin-0.9.31 → margin-0.10.0}/LICENSE +0 -0
  20. {margin-0.9.31 → margin-0.10.0}/NOTICE +0 -0
  21. {margin-0.9.31 → margin-0.10.0}/margin/algebra.py +0 -0
  22. {margin-0.9.31 → margin-0.10.0}/margin/anomaly.py +0 -0
  23. {margin-0.9.31 → margin-0.10.0}/margin/bridge.py +0 -0
  24. {margin-0.9.31 → margin-0.10.0}/margin/calibrate.py +0 -0
  25. {margin-0.9.31 → margin-0.10.0}/margin/confidence.py +0 -0
  26. {margin-0.9.31 → margin-0.10.0}/margin/config.py +0 -0
  27. {margin-0.9.31 → margin-0.10.0}/margin/contract.py +0 -0
  28. {margin-0.9.31 → margin-0.10.0}/margin/controller.py +0 -0
  29. {margin-0.9.31 → margin-0.10.0}/margin/correlate.py +0 -0
  30. {margin-0.9.31 → margin-0.10.0}/margin/drift.py +0 -0
  31. {margin-0.9.31 → margin-0.10.0}/margin/events.py +0 -0
  32. {margin-0.9.31 → margin-0.10.0}/margin/fingerprint.py +0 -0
  33. {margin-0.9.31 → margin-0.10.0}/margin/flow.py +0 -0
  34. {margin-0.9.31 → margin-0.10.0}/margin/forecast.py +0 -0
  35. {margin-0.9.31 → margin-0.10.0}/margin/health.py +0 -0
  36. {margin-0.9.31 → margin-0.10.0}/margin/intent.py +0 -0
  37. {margin-0.9.31 → margin-0.10.0}/margin/issue.py +0 -0
  38. {margin-0.9.31 → margin-0.10.0}/margin/ledger.py +0 -0
  39. {margin-0.9.31 → margin-0.10.0}/margin/observation.py +0 -0
  40. {margin-0.9.31 → margin-0.10.0}/margin/persist.py +0 -0
  41. {margin-0.9.31 → margin-0.10.0}/margin/policy/__init__.py +0 -0
  42. {margin-0.9.31 → margin-0.10.0}/margin/policy/compose.py +0 -0
  43. {margin-0.9.31 → margin-0.10.0}/margin/policy/core.py +0 -0
  44. {margin-0.9.31 → margin-0.10.0}/margin/policy/temporal.py +0 -0
  45. {margin-0.9.31 → margin-0.10.0}/margin/policy/trace.py +0 -0
  46. {margin-0.9.31 → margin-0.10.0}/margin/policy/tuning.py +0 -0
  47. {margin-0.9.31 → margin-0.10.0}/margin/policy/validate.py +0 -0
  48. {margin-0.9.31 → margin-0.10.0}/margin/predicates.py +0 -0
  49. {margin-0.9.31 → margin-0.10.0}/margin/streaming.py +0 -0
  50. {margin-0.9.31 → margin-0.10.0}/margin/transitions.py +0 -0
  51. {margin-0.9.31 → margin-0.10.0}/margin/validity.py +0 -0
  52. {margin-0.9.31 → margin-0.10.0}/margin.egg-info/dependency_links.txt +0 -0
  53. {margin-0.9.31 → margin-0.10.0}/margin.egg-info/top_level.txt +0 -0
  54. {margin-0.9.31 → margin-0.10.0}/setup.cfg +0 -0
  55. {margin-0.9.31 → margin-0.10.0}/tests/test_absence.py +0 -0
  56. {margin-0.9.31 → margin-0.10.0}/tests/test_algebra.py +0 -0
  57. {margin-0.9.31 → margin-0.10.0}/tests/test_anomaly.py +0 -0
  58. {margin-0.9.31 → margin-0.10.0}/tests/test_bridge.py +0 -0
  59. {margin-0.9.31 → margin-0.10.0}/tests/test_calibrate.py +0 -0
  60. {margin-0.9.31 → margin-0.10.0}/tests/test_causal.py +0 -0
  61. {margin-0.9.31 → margin-0.10.0}/tests/test_composite.py +0 -0
  62. {margin-0.9.31 → margin-0.10.0}/tests/test_confidence.py +0 -0
  63. {margin-0.9.31 → margin-0.10.0}/tests/test_config.py +0 -0
  64. {margin-0.9.31 → margin-0.10.0}/tests/test_contract.py +0 -0
  65. {margin-0.9.31 → margin-0.10.0}/tests/test_controller.py +0 -0
  66. {margin-0.9.31 → margin-0.10.0}/tests/test_correlate.py +0 -0
  67. {margin-0.9.31 → margin-0.10.0}/tests/test_diff.py +0 -0
  68. {margin-0.9.31 → margin-0.10.0}/tests/test_drift.py +0 -0
  69. {margin-0.9.31 → margin-0.10.0}/tests/test_events.py +0 -0
  70. {margin-0.9.31 → margin-0.10.0}/tests/test_fingerprint_ext.py +0 -0
  71. {margin-0.9.31 → margin-0.10.0}/tests/test_float_tolerance.py +0 -0
  72. {margin-0.9.31 → margin-0.10.0}/tests/test_flow.py +0 -0
  73. {margin-0.9.31 → margin-0.10.0}/tests/test_forecast.py +0 -0
  74. {margin-0.9.31 → margin-0.10.0}/tests/test_health.py +0 -0
  75. {margin-0.9.31 → margin-0.10.0}/tests/test_intent.py +0 -0
  76. {margin-0.9.31 → margin-0.10.0}/tests/test_issue.py +0 -0
  77. {margin-0.9.31 → margin-0.10.0}/tests/test_ledger.py +0 -0
  78. {margin-0.9.31 → margin-0.10.0}/tests/test_loop.py +0 -0
  79. {margin-0.9.31 → margin-0.10.0}/tests/test_observation.py +0 -0
  80. {margin-0.9.31 → margin-0.10.0}/tests/test_persist.py +0 -0
  81. {margin-0.9.31 → margin-0.10.0}/tests/test_pipeline_features.py +0 -0
  82. {margin-0.9.31 → margin-0.10.0}/tests/test_policy.py +0 -0
  83. {margin-0.9.31 → margin-0.10.0}/tests/test_policy_compose.py +0 -0
  84. {margin-0.9.31 → margin-0.10.0}/tests/test_policy_temporal.py +0 -0
  85. {margin-0.9.31 → margin-0.10.0}/tests/test_policy_trace.py +0 -0
  86. {margin-0.9.31 → margin-0.10.0}/tests/test_policy_tuning.py +0 -0
  87. {margin-0.9.31 → margin-0.10.0}/tests/test_policy_validate.py +0 -0
  88. {margin-0.9.31 → margin-0.10.0}/tests/test_predicates.py +0 -0
  89. {margin-0.9.31 → margin-0.10.0}/tests/test_provenance.py +0 -0
  90. {margin-0.9.31 → margin-0.10.0}/tests/test_reverse_bridge.py +0 -0
  91. {margin-0.9.31 → margin-0.10.0}/tests/test_staleness.py +0 -0
  92. {margin-0.9.31 → margin-0.10.0}/tests/test_streaming.py +0 -0
  93. {margin-0.9.31 → margin-0.10.0}/tests/test_transitions.py +0 -0
  94. {margin-0.9.31 → margin-0.10.0}/tests/test_uncertain.py +0 -0
  95. {margin-0.9.31 → margin-0.10.0}/tests/test_validity.py +0 -0
  96. {margin-0.9.31 → 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.31
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
 
@@ -86,6 +111,8 @@ Then the correction loop:
86
111
  - **Causal** — dependency graphs ("api is DEGRADED because db is ABLATED")
87
112
  - **Auto-correlation** — discover which components move together from data, with lag detection
88
113
  - **Streaming** — incremental trackers: `Monitor.update(values)` updates health + drift + anomaly + correlation in one call
114
+ - **Flow** — ordered stages with typed live guidance: ADVANCE / HOLD / RETRY / ROLLBACK / ESCALATE for multi-stage processes
115
+ - **Issue** — typed diagnostics: library surfaces its own misconfigurations, degraded assumptions, recovered failures
89
116
  - **Config** — define everything in YAML/JSON: `margin.load_config("margin.yaml")`
90
117
  - **Persistence** — save/restore Monitor state across restarts, batch replay from CSV
91
118
  - **Intent** — goal feasibility: `intent.evaluate_monitor(monitor)` → FEASIBLE / AT_RISK / INFEASIBLE with ETA
@@ -123,6 +150,55 @@ Thresholds(intact=0.02, ablated=0.10, higher_is_better=False)
123
150
 
124
151
  One flag. Threads through every comparison, every sigma calculation, every correction decision, every recovery ratio. You never think about it again.
125
152
 
153
+ ## Multi-stage process guidance
154
+
155
+ For processes where each stage has distinct entry/quality/exit criteria — deployment pipelines, fermentation phases, clinical trials, incident response:
156
+
157
+ ```python
158
+ from margin import Flow, Stage, Parser, Thresholds
159
+
160
+ flow = Flow(stages=[
161
+ Stage(
162
+ name="lag",
163
+ parser=Parser(baselines={"biomass": 0.1}, thresholds=Thresholds(intact=0.08, ablated=0.02)),
164
+ advance_when=lambda expr: expr.observations[0].value >= 0.15,
165
+ ),
166
+ Stage(name="exponential", parser=..., advance_when=...),
167
+ Stage(name="stationary", parser=...),
168
+ ], label="ferment")
169
+
170
+ result = flow.observe({"biomass": 0.12})
171
+ print(result.to_atom())
172
+ # flow:ferment:lag[1/3] → HOLD
173
+
174
+ # When the advance_when predicate fires:
175
+ result = flow.observe({"biomass": 0.18})
176
+ # result.guidance == Guidance.ADVANCE
177
+ flow.advance()
178
+ ```
179
+
180
+ Predicate exceptions are caught as ERROR issues, guidance degrades to HOLD, the flow stays alive — safety-first semantics for safety-critical contexts.
181
+
182
+ ## Typed diagnostics
183
+
184
+ The library surfaces its own operational state the same way it classifies observed state:
185
+
186
+ ```python
187
+ monitor.update({"mystery": 1.0}) # emits WARNING: BASELINE_MISSING
188
+ monitor.drift("unknown") # emits WARNING: UNKNOWN_COMPONENT
189
+
190
+ # Query it programmatically
191
+ monitor.issues.by_code("BASELINE_MISSING") # [Issue(...)]
192
+ monitor.issues.at_least(IssueLevel.WARNING) # all severe enough
193
+ monitor.diagnose() # JSON-safe dashboard summary
194
+
195
+ # Bridge to your logger
196
+ import logging
197
+ monitor.issues.attach_logger(logging.getLogger("margin"))
198
+ ```
199
+
200
+ **Harvest-safe on hard fail:** if `Monitor.update()` or `Flow.observe()` raises, a `FATAL(INTERNAL_ERROR)` issue is recorded *before* the exception propagates. `monitor.issues` survives crashes intact.
201
+
126
202
  ## Auto-calibrate from data
127
203
 
128
204
  Don't guess thresholds. Derive them from healthy measurements:
@@ -147,33 +223,28 @@ parser = parser_from_calibration(
147
223
  | **Contract** | Are we meeting our goals? | `HealthTarget`, `SustainHealth`, `RecoveryThreshold` |
148
224
  | **Causal** | Why did this happen? | `CausalGraph`, `CausalLink`, `Explanation` |
149
225
  | **Intent** | Can we still get there? | `Intent`, `Feasibility`, `IntentResult` |
226
+ | **Flow** | What stage are we in, and what should we do? | `Flow`, `Stage`, `Guidance`, `FlowResult` |
227
+ | **Issue** | What did the library notice about itself? | `Issue`, `IssueLevel`, `IssueBuffer`, `IssueCode` |
150
228
 
151
- `full_step()` orchestrates all seven in one call.
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.
152
230
 
153
- ## Domain adapters
231
+ ## Adapters
154
232
 
155
- 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:
156
234
 
157
- | Adapter | What it monitors | Polarity |
158
- |---|---|---|
159
- | [**healthcare**](https://github.com/Cope-Labs/margin/tree/main/adapters/healthcare/) | Vital signs (HR, BP, SpO2, temp, glucose) — WHO/AHA ranges, sepsis screening | bands |
160
- | [**godot**](https://github.com/sethc5/margin-godot) | Game systems (food, morale, stress, stamina) — native GDScript | mixed |
161
- | [**homeassistant**](https://github.com/Cope-Labs/margin/tree/main/adapters/homeassistant/) | Smart home sensors (temp, humidity, battery, solar, power) | mixed |
162
- | [**evcharging**](https://github.com/Cope-Labs/margin/tree/main/adapters/evcharging/) | EV charging (SoC, grid draw, solar surplus, efficiency) | mixed |
163
- | [**infrastructure**](https://github.com/Cope-Labs/margin/tree/main/adapters/infrastructure/) | Server monitoring (CPU, memory, disk, latency, error rate) | mixed |
164
- | [**aquarium**](https://github.com/Cope-Labs/margin/tree/main/adapters/aquarium/) | Water chemistry (pH, ammonia, nitrite, temp, hardness) | bands |
165
- | [**greenhouse**](https://github.com/Cope-Labs/margin/tree/main/adapters/greenhouse/) | Growing environment (soil moisture, CO₂, light, VPD) | bands |
166
- | [**fitness**](https://github.com/Cope-Labs/margin/tree/main/adapters/fitness/) | Wearables (resting HR, HRV, sleep, steps, stress) | mixed |
167
- | [**transformer**](https://github.com/Cope-Labs/margin/tree/main/adapters/transformer/) | ML circuit interpretability (pythia-6.9b) | higher |
168
- | [**fastapi**](https://github.com/Cope-Labs/margin/tree/main/adapters/fastapi/) | Endpoint health (latency, error rate, throughput, queue depth) | mixed |
169
- | [**database**](https://github.com/Cope-Labs/margin/tree/main/adapters/database/) | DB health (pool usage, query latency, replication lag, deadlocks) | mixed |
170
- | [**celery**](https://github.com/Cope-Labs/margin/tree/main/adapters/celery/) | Task queue (queue depth, failure rate, worker utilization, retries) | mixed |
171
- | [**dataframe**](https://github.com/Cope-Labs/margin/tree/main/adapters/dataframe/) | Data quality (completeness, null rate, drift, freshness, schema) | mixed |
172
- | [**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.
173
244
 
174
245
  ## Docs
175
246
 
176
- 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.
177
248
 
178
249
  ## License
179
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
 
@@ -57,6 +82,8 @@ Then the correction loop:
57
82
  - **Causal** — dependency graphs ("api is DEGRADED because db is ABLATED")
58
83
  - **Auto-correlation** — discover which components move together from data, with lag detection
59
84
  - **Streaming** — incremental trackers: `Monitor.update(values)` updates health + drift + anomaly + correlation in one call
85
+ - **Flow** — ordered stages with typed live guidance: ADVANCE / HOLD / RETRY / ROLLBACK / ESCALATE for multi-stage processes
86
+ - **Issue** — typed diagnostics: library surfaces its own misconfigurations, degraded assumptions, recovered failures
60
87
  - **Config** — define everything in YAML/JSON: `margin.load_config("margin.yaml")`
61
88
  - **Persistence** — save/restore Monitor state across restarts, batch replay from CSV
62
89
  - **Intent** — goal feasibility: `intent.evaluate_monitor(monitor)` → FEASIBLE / AT_RISK / INFEASIBLE with ETA
@@ -94,6 +121,55 @@ Thresholds(intact=0.02, ablated=0.10, higher_is_better=False)
94
121
 
95
122
  One flag. Threads through every comparison, every sigma calculation, every correction decision, every recovery ratio. You never think about it again.
96
123
 
124
+ ## Multi-stage process guidance
125
+
126
+ For processes where each stage has distinct entry/quality/exit criteria — deployment pipelines, fermentation phases, clinical trials, incident response:
127
+
128
+ ```python
129
+ from margin import Flow, Stage, Parser, Thresholds
130
+
131
+ flow = Flow(stages=[
132
+ Stage(
133
+ name="lag",
134
+ parser=Parser(baselines={"biomass": 0.1}, thresholds=Thresholds(intact=0.08, ablated=0.02)),
135
+ advance_when=lambda expr: expr.observations[0].value >= 0.15,
136
+ ),
137
+ Stage(name="exponential", parser=..., advance_when=...),
138
+ Stage(name="stationary", parser=...),
139
+ ], label="ferment")
140
+
141
+ result = flow.observe({"biomass": 0.12})
142
+ print(result.to_atom())
143
+ # flow:ferment:lag[1/3] → HOLD
144
+
145
+ # When the advance_when predicate fires:
146
+ result = flow.observe({"biomass": 0.18})
147
+ # result.guidance == Guidance.ADVANCE
148
+ flow.advance()
149
+ ```
150
+
151
+ Predicate exceptions are caught as ERROR issues, guidance degrades to HOLD, the flow stays alive — safety-first semantics for safety-critical contexts.
152
+
153
+ ## Typed diagnostics
154
+
155
+ The library surfaces its own operational state the same way it classifies observed state:
156
+
157
+ ```python
158
+ monitor.update({"mystery": 1.0}) # emits WARNING: BASELINE_MISSING
159
+ monitor.drift("unknown") # emits WARNING: UNKNOWN_COMPONENT
160
+
161
+ # Query it programmatically
162
+ monitor.issues.by_code("BASELINE_MISSING") # [Issue(...)]
163
+ monitor.issues.at_least(IssueLevel.WARNING) # all severe enough
164
+ monitor.diagnose() # JSON-safe dashboard summary
165
+
166
+ # Bridge to your logger
167
+ import logging
168
+ monitor.issues.attach_logger(logging.getLogger("margin"))
169
+ ```
170
+
171
+ **Harvest-safe on hard fail:** if `Monitor.update()` or `Flow.observe()` raises, a `FATAL(INTERNAL_ERROR)` issue is recorded *before* the exception propagates. `monitor.issues` survives crashes intact.
172
+
97
173
  ## Auto-calibrate from data
98
174
 
99
175
  Don't guess thresholds. Derive them from healthy measurements:
@@ -118,33 +194,28 @@ parser = parser_from_calibration(
118
194
  | **Contract** | Are we meeting our goals? | `HealthTarget`, `SustainHealth`, `RecoveryThreshold` |
119
195
  | **Causal** | Why did this happen? | `CausalGraph`, `CausalLink`, `Explanation` |
120
196
  | **Intent** | Can we still get there? | `Intent`, `Feasibility`, `IntentResult` |
197
+ | **Flow** | What stage are we in, and what should we do? | `Flow`, `Stage`, `Guidance`, `FlowResult` |
198
+ | **Issue** | What did the library notice about itself? | `Issue`, `IssueLevel`, `IssueBuffer`, `IssueCode` |
121
199
 
122
- `full_step()` orchestrates all seven in one call.
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.
123
201
 
124
- ## Domain adapters
202
+ ## Adapters
125
203
 
126
- 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:
127
205
 
128
- | Adapter | What it monitors | Polarity |
129
- |---|---|---|
130
- | [**healthcare**](https://github.com/Cope-Labs/margin/tree/main/adapters/healthcare/) | Vital signs (HR, BP, SpO2, temp, glucose) — WHO/AHA ranges, sepsis screening | bands |
131
- | [**godot**](https://github.com/sethc5/margin-godot) | Game systems (food, morale, stress, stamina) — native GDScript | mixed |
132
- | [**homeassistant**](https://github.com/Cope-Labs/margin/tree/main/adapters/homeassistant/) | Smart home sensors (temp, humidity, battery, solar, power) | mixed |
133
- | [**evcharging**](https://github.com/Cope-Labs/margin/tree/main/adapters/evcharging/) | EV charging (SoC, grid draw, solar surplus, efficiency) | mixed |
134
- | [**infrastructure**](https://github.com/Cope-Labs/margin/tree/main/adapters/infrastructure/) | Server monitoring (CPU, memory, disk, latency, error rate) | mixed |
135
- | [**aquarium**](https://github.com/Cope-Labs/margin/tree/main/adapters/aquarium/) | Water chemistry (pH, ammonia, nitrite, temp, hardness) | bands |
136
- | [**greenhouse**](https://github.com/Cope-Labs/margin/tree/main/adapters/greenhouse/) | Growing environment (soil moisture, CO₂, light, VPD) | bands |
137
- | [**fitness**](https://github.com/Cope-Labs/margin/tree/main/adapters/fitness/) | Wearables (resting HR, HRV, sleep, steps, stress) | mixed |
138
- | [**transformer**](https://github.com/Cope-Labs/margin/tree/main/adapters/transformer/) | ML circuit interpretability (pythia-6.9b) | higher |
139
- | [**fastapi**](https://github.com/Cope-Labs/margin/tree/main/adapters/fastapi/) | Endpoint health (latency, error rate, throughput, queue depth) | mixed |
140
- | [**database**](https://github.com/Cope-Labs/margin/tree/main/adapters/database/) | DB health (pool usage, query latency, replication lag, deadlocks) | mixed |
141
- | [**celery**](https://github.com/Cope-Labs/margin/tree/main/adapters/celery/) | Task queue (queue depth, failure rate, worker utilization, retries) | mixed |
142
- | [**dataframe**](https://github.com/Cope-Labs/margin/tree/main/adapters/dataframe/) | Data quality (completeness, null rate, drift, freshness, schema) | mixed |
143
- | [**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.
144
215
 
145
216
  ## Docs
146
217
 
147
- 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.
148
219
 
149
220
  ## License
150
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(