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.
- {margin-0.9.32/margin.egg-info → margin-0.10.0}/PKG-INFO +42 -24
- {margin-0.9.32 → margin-0.10.0}/README.md +41 -23
- {margin-0.9.32 → margin-0.10.0}/margin/__init__.py +44 -21
- {margin-0.9.32 → margin-0.10.0}/margin/__main__.py +19 -0
- {margin-0.9.32 → margin-0.10.0}/margin/causal.py +13 -0
- {margin-0.9.32 → margin-0.10.0}/margin/composite.py +4 -0
- {margin-0.9.32 → margin-0.10.0}/margin/diff.py +54 -16
- {margin-0.9.32 → margin-0.10.0}/margin/loop.py +44 -0
- {margin-0.9.32 → margin-0.10.0}/margin/provenance.py +0 -3
- {margin-0.9.32 → margin-0.10.0}/margin/uncertain.py +0 -6
- {margin-0.9.32 → margin-0.10.0/margin.egg-info}/PKG-INFO +42 -24
- {margin-0.9.32 → margin-0.10.0}/margin.egg-info/SOURCES.txt +0 -5
- {margin-0.9.32 → margin-0.10.0}/pyproject.toml +1 -1
- margin-0.9.32/tests/test_healthcare.py +0 -210
- margin-0.9.32/tests/test_neuro.py +0 -159
- margin-0.9.32/tests/test_printer3d.py +0 -151
- margin-0.9.32/tests/test_ros2.py +0 -190
- margin-0.9.32/tests/test_transformer.py +0 -301
- {margin-0.9.32 → margin-0.10.0}/LICENSE +0 -0
- {margin-0.9.32 → margin-0.10.0}/NOTICE +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/algebra.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/anomaly.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/bridge.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/calibrate.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/confidence.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/config.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/contract.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/controller.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/correlate.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/drift.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/events.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/fingerprint.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/flow.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/forecast.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/health.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/intent.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/issue.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/ledger.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/observation.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/persist.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/policy/__init__.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/policy/compose.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/policy/core.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/policy/temporal.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/policy/trace.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/policy/tuning.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/policy/validate.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/predicates.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/streaming.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/transitions.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin/validity.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin.egg-info/dependency_links.txt +0 -0
- {margin-0.9.32 → margin-0.10.0}/margin.egg-info/top_level.txt +0 -0
- {margin-0.9.32 → margin-0.10.0}/setup.cfg +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_absence.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_algebra.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_anomaly.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_bridge.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_calibrate.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_causal.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_composite.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_confidence.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_config.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_contract.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_controller.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_correlate.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_diff.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_drift.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_events.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_fingerprint_ext.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_float_tolerance.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_flow.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_forecast.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_health.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_intent.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_issue.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_ledger.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_loop.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_observation.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_persist.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_pipeline_features.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_policy.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_policy_compose.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_policy_temporal.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_policy_trace.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_policy_tuning.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_policy_validate.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_predicates.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_provenance.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_reverse_bridge.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_staleness.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_streaming.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_transitions.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_uncertain.py +0 -0
- {margin-0.9.32 → margin-0.10.0}/tests/test_validity.py +0 -0
- {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.
|
|
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
|
[](https://pepy.tech/project/margin)
|
|
35
35
|
[](https://opensource.org/licenses/MIT)
|
|
36
36
|
|
|
37
|
-
**Typed
|
|
37
|
+
**Typed primitives for system self-awareness.**
|
|
38
38
|
|
|
39
|
-
|
|
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
|
|
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
|
-
##
|
|
231
|
+
## Adapters
|
|
207
232
|
|
|
208
|
-
|
|
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 |
|
|
211
|
-
|
|
212
|
-
| [**
|
|
213
|
-
| [**
|
|
214
|
-
| [**
|
|
215
|
-
| [**
|
|
216
|
-
| [**
|
|
217
|
-
|
|
218
|
-
|
|
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
|
[](https://pepy.tech/project/margin)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
**Typed
|
|
8
|
+
**Typed primitives for system self-awareness.**
|
|
9
9
|
|
|
10
|
-
|
|
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
|
|
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
|
-
##
|
|
202
|
+
## Adapters
|
|
178
203
|
|
|
179
|
-
|
|
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 |
|
|
182
|
-
|
|
183
|
-
| [**
|
|
184
|
-
| [**
|
|
185
|
-
| [**
|
|
186
|
-
| [**
|
|
187
|
-
| [**
|
|
188
|
-
|
|
189
|
-
|
|
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,
|
|
21
|
+
new_id, _new_id, are_correlated, merge,
|
|
22
22
|
ProvenanceNode, ProvenanceGraph, create_root_provenance,
|
|
23
23
|
)
|
|
24
|
-
from .uncertain import UncertainValue, Source
|
|
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
|
-
#
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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:
|
|
21
|
-
health_before:
|
|
22
|
-
health_after:
|
|
23
|
-
sigma_before:
|
|
24
|
-
sigma_after:
|
|
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
|
|
69
|
-
a = self.health_after
|
|
70
|
-
if
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return f"{self.name}: {a} (
|
|
76
|
-
return f"{self.name}: {
|
|
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
|
-
|
|
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:
|