margin 0.9.26__tar.gz → 0.9.28__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.26/margin.egg-info → margin-0.9.28}/PKG-INFO +20 -20
- {margin-0.9.26 → margin-0.9.28}/README.md +14 -14
- {margin-0.9.26 → margin-0.9.28}/margin/__init__.py +5 -0
- margin-0.9.28/margin/flow.py +390 -0
- {margin-0.9.26 → margin-0.9.28/margin.egg-info}/PKG-INFO +20 -20
- {margin-0.9.26 → margin-0.9.28}/margin.egg-info/SOURCES.txt +2 -0
- {margin-0.9.26 → margin-0.9.28}/pyproject.toml +6 -6
- margin-0.9.28/tests/test_flow.py +418 -0
- {margin-0.9.26 → margin-0.9.28}/LICENSE +0 -0
- {margin-0.9.26 → margin-0.9.28}/NOTICE +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/__main__.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/algebra.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/anomaly.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/bridge.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/calibrate.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/causal.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/composite.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/confidence.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/config.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/contract.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/controller.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/correlate.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/diff.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/drift.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/events.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/fingerprint.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/forecast.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/health.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/intent.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/ledger.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/loop.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/observation.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/persist.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/policy/__init__.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/policy/compose.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/policy/core.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/policy/temporal.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/policy/trace.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/policy/tuning.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/policy/validate.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/predicates.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/provenance.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/streaming.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/transitions.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/uncertain.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin/validity.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin.egg-info/dependency_links.txt +0 -0
- {margin-0.9.26 → margin-0.9.28}/margin.egg-info/top_level.txt +0 -0
- {margin-0.9.26 → margin-0.9.28}/setup.cfg +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_absence.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_algebra.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_anomaly.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_bridge.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_calibrate.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_causal.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_composite.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_confidence.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_config.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_contract.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_controller.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_correlate.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_diff.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_drift.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_events.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_fingerprint_ext.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_float_tolerance.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_forecast.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_health.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_healthcare.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_intent.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_ledger.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_loop.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_neuro.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_observation.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_persist.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_pipeline_features.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_policy.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_policy_compose.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_policy_temporal.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_policy_trace.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_policy_tuning.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_policy_validate.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_predicates.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_printer3d.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_provenance.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_reverse_bridge.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_ros2.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_staleness.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_streaming.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_transformer.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_transitions.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_uncertain.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_validity.py +0 -0
- {margin-0.9.26 → margin-0.9.28}/tests/test_windowing.py +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: margin
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.28
|
|
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
|
|
7
|
-
Project-URL: Homepage, https://
|
|
8
|
-
Project-URL: Repository, https://github.com/
|
|
9
|
-
Project-URL: Issues, https://github.com/
|
|
10
|
-
Project-URL: Changelog, https://github.com/
|
|
11
|
-
Project-URL: Documentation, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://copelabs.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/Cope-Labs/margin
|
|
9
|
+
Project-URL: Issues, https://github.com/Cope-Labs/margin/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Cope-Labs/margin/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Documentation, https://github.com/Cope-Labs/margin#readme
|
|
12
12
|
Keywords: health,monitoring,uncertainty,observability,typed,classification,threshold,polarity,correction,audit,ledger,policy
|
|
13
13
|
Classifier: Development Status :: 4 - Beta
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
@@ -156,24 +156,24 @@ Ready-to-use threshold profiles for specific domains:
|
|
|
156
156
|
|
|
157
157
|
| Adapter | What it monitors | Polarity |
|
|
158
158
|
|---|---|---|
|
|
159
|
-
| [**healthcare**](https://github.com/
|
|
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
160
|
| [**godot**](https://github.com/sethc5/margin-godot) | Game systems (food, morale, stress, stamina) — native GDScript | mixed |
|
|
161
|
-
| [**homeassistant**](https://github.com/
|
|
162
|
-
| [**evcharging**](https://github.com/
|
|
163
|
-
| [**infrastructure**](https://github.com/
|
|
164
|
-
| [**aquarium**](https://github.com/
|
|
165
|
-
| [**greenhouse**](https://github.com/
|
|
166
|
-
| [**fitness**](https://github.com/
|
|
167
|
-
| [**transformer**](https://github.com/
|
|
168
|
-
| [**fastapi**](https://github.com/
|
|
169
|
-
| [**database**](https://github.com/
|
|
170
|
-
| [**celery**](https://github.com/
|
|
171
|
-
| [**dataframe**](https://github.com/
|
|
172
|
-
| [**pytest**](https://github.com/
|
|
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 |
|
|
173
173
|
|
|
174
174
|
## Docs
|
|
175
175
|
|
|
176
|
-
Full specification: [margin-language.md](https://github.com/
|
|
176
|
+
Full specification: [margin-language.md](https://github.com/Cope-Labs/margin/blob/main/margin/margin-language.md)
|
|
177
177
|
|
|
178
178
|
## License
|
|
179
179
|
|
|
@@ -127,24 +127,24 @@ Ready-to-use threshold profiles for specific domains:
|
|
|
127
127
|
|
|
128
128
|
| Adapter | What it monitors | Polarity |
|
|
129
129
|
|---|---|---|
|
|
130
|
-
| [**healthcare**](https://github.com/
|
|
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
131
|
| [**godot**](https://github.com/sethc5/margin-godot) | Game systems (food, morale, stress, stamina) — native GDScript | mixed |
|
|
132
|
-
| [**homeassistant**](https://github.com/
|
|
133
|
-
| [**evcharging**](https://github.com/
|
|
134
|
-
| [**infrastructure**](https://github.com/
|
|
135
|
-
| [**aquarium**](https://github.com/
|
|
136
|
-
| [**greenhouse**](https://github.com/
|
|
137
|
-
| [**fitness**](https://github.com/
|
|
138
|
-
| [**transformer**](https://github.com/
|
|
139
|
-
| [**fastapi**](https://github.com/
|
|
140
|
-
| [**database**](https://github.com/
|
|
141
|
-
| [**celery**](https://github.com/
|
|
142
|
-
| [**dataframe**](https://github.com/
|
|
143
|
-
| [**pytest**](https://github.com/
|
|
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 |
|
|
144
144
|
|
|
145
145
|
## Docs
|
|
146
146
|
|
|
147
|
-
Full specification: [margin-language.md](https://github.com/
|
|
147
|
+
Full specification: [margin-language.md](https://github.com/Cope-Labs/margin/blob/main/margin/margin-language.md)
|
|
148
148
|
|
|
149
149
|
## License
|
|
150
150
|
|
|
@@ -120,6 +120,9 @@ from .intent import (
|
|
|
120
120
|
# Loop
|
|
121
121
|
from .loop import StepResult, step, run, FullStepResult, full_step
|
|
122
122
|
|
|
123
|
+
# Flow
|
|
124
|
+
from .flow import Flow, Stage, Guidance, FlowResult, GUIDANCE_SEVERITY
|
|
125
|
+
|
|
123
126
|
__all__ = [
|
|
124
127
|
# Foundation
|
|
125
128
|
"Confidence",
|
|
@@ -190,6 +193,8 @@ __all__ = [
|
|
|
190
193
|
"save_monitor", "load_monitor", "replay", "replay_csv",
|
|
191
194
|
# Loop
|
|
192
195
|
"StepResult", "step", "run", "FullStepResult", "full_step",
|
|
196
|
+
# Flow
|
|
197
|
+
"Flow", "Stage", "Guidance", "FlowResult", "GUIDANCE_SEVERITY",
|
|
193
198
|
# margin-poc compat aliases
|
|
194
199
|
"EpistemicSource", "UncertaintyMode",
|
|
195
200
|
"CallConfidence", "CircuitHealth", "GapThresholds", "MarginParser",
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flow: ordered sequence of stages with typed guidance.
|
|
3
|
+
|
|
4
|
+
A Flow is an ordered list of Stage objects, each with its own Parser and
|
|
5
|
+
per-stage Monitor. Observations are routed to the current stage; the Flow
|
|
6
|
+
emits typed Guidance (ADVANCE / HOLD / RETRY / ROLLBACK / ESCALATE /
|
|
7
|
+
COMPLETE) based on stage health and user-supplied advance/retry/escalate
|
|
8
|
+
predicates.
|
|
9
|
+
|
|
10
|
+
Minimal use::
|
|
11
|
+
|
|
12
|
+
from margin import Flow, Stage, Parser, Thresholds, Guidance
|
|
13
|
+
|
|
14
|
+
ferment = Flow(stages=[
|
|
15
|
+
Stage(
|
|
16
|
+
name="inoculation",
|
|
17
|
+
parser=Parser(baselines={"pH": 6.5, "DO": 90}, thresholds=Thresholds(...)),
|
|
18
|
+
advance_when=lambda expr: expr.health_of("DO") == Health.INTACT,
|
|
19
|
+
),
|
|
20
|
+
Stage(
|
|
21
|
+
name="lag_phase",
|
|
22
|
+
parser=Parser(baselines={"pH": 6.3, "biomass": 0.1}, thresholds=Thresholds(...)),
|
|
23
|
+
advance_when=lambda expr: (expr.health_of("biomass") == Health.INTACT
|
|
24
|
+
and expr.observations[0].sigma > 0.5),
|
|
25
|
+
),
|
|
26
|
+
# ...
|
|
27
|
+
], label="fermentation")
|
|
28
|
+
|
|
29
|
+
result = ferment.observe({"pH": 6.5, "DO": 88})
|
|
30
|
+
# → FlowResult(stage="inoculation", guidance=HOLD, reason=...)
|
|
31
|
+
|
|
32
|
+
# When ready:
|
|
33
|
+
result = ferment.observe({"pH": 6.4, "DO": 92})
|
|
34
|
+
# → FlowResult(stage="inoculation", guidance=ADVANCE, ...)
|
|
35
|
+
ferment.advance()
|
|
36
|
+
|
|
37
|
+
# Compact rendering:
|
|
38
|
+
result.to_atom()
|
|
39
|
+
# → "flow:fermentation:inoculation[1/5] → ADVANCE"
|
|
40
|
+
|
|
41
|
+
Contracts can be used in predicates::
|
|
42
|
+
|
|
43
|
+
from margin import Contract, HealthTarget, Health, Ledger
|
|
44
|
+
contract = Contract(terms=[HealthTarget(component="biomass", target=Health.INTACT)])
|
|
45
|
+
ledger = Ledger()
|
|
46
|
+
stage = Stage(
|
|
47
|
+
name="stationary",
|
|
48
|
+
parser=parser,
|
|
49
|
+
advance_when=lambda expr: contract.evaluate(ledger, expr).passed,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
This keeps Flow lightweight (no mandatory Ledger) while letting advanced
|
|
53
|
+
users plug in the full Contract machinery when they need it.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
from dataclasses import dataclass, field
|
|
59
|
+
from datetime import datetime
|
|
60
|
+
from enum import Enum
|
|
61
|
+
from typing import Callable, Dict, List, Optional
|
|
62
|
+
|
|
63
|
+
from .health import Health
|
|
64
|
+
from .observation import Expression, Parser
|
|
65
|
+
from .streaming import Monitor
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# -----------------------------------------------------------------------
|
|
69
|
+
# Guidance
|
|
70
|
+
# -----------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
class Guidance(Enum):
|
|
73
|
+
"""
|
|
74
|
+
Recommendation for the current flow state.
|
|
75
|
+
|
|
76
|
+
ADVANCE: exit criteria satisfied — ready to move to the next stage
|
|
77
|
+
HOLD: stage running healthy, exit criteria not yet met
|
|
78
|
+
RETRY: stage degraded but recoverable; continue observing
|
|
79
|
+
ROLLBACK: this stage cannot proceed; return to previous stage
|
|
80
|
+
ESCALATE: cannot proceed automatically — needs human intervention
|
|
81
|
+
COMPLETE: all stages are done
|
|
82
|
+
"""
|
|
83
|
+
ADVANCE = "ADVANCE"
|
|
84
|
+
HOLD = "HOLD"
|
|
85
|
+
RETRY = "RETRY"
|
|
86
|
+
ROLLBACK = "ROLLBACK"
|
|
87
|
+
ESCALATE = "ESCALATE"
|
|
88
|
+
COMPLETE = "COMPLETE"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
GUIDANCE_SEVERITY: Dict[Guidance, int] = {
|
|
92
|
+
Guidance.COMPLETE: 0,
|
|
93
|
+
Guidance.ADVANCE: 0,
|
|
94
|
+
Guidance.HOLD: 1,
|
|
95
|
+
Guidance.RETRY: 2,
|
|
96
|
+
Guidance.ROLLBACK: 3,
|
|
97
|
+
Guidance.ESCALATE: 4,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# -----------------------------------------------------------------------
|
|
102
|
+
# Stage
|
|
103
|
+
# -----------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
StagePredicate = Callable[[Expression], bool]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class Stage:
|
|
110
|
+
"""
|
|
111
|
+
One stage in a process flow.
|
|
112
|
+
|
|
113
|
+
name: human-readable stage identifier
|
|
114
|
+
parser: Parser for measurements observed during this stage
|
|
115
|
+
advance_when: predicate(expr) → True means exit criteria met
|
|
116
|
+
retry_when: predicate(expr) → True means stage is degraded-but-recoverable
|
|
117
|
+
escalate_when: predicate(expr) → True means stage cannot proceed
|
|
118
|
+
rollback_when: predicate(expr) → True means return to the previous stage
|
|
119
|
+
window: observation window size for the stage's Monitor (default 20)
|
|
120
|
+
|
|
121
|
+
All predicates are optional. When none fire, Guidance defaults to HOLD.
|
|
122
|
+
If no explicit predicates are given, the default classifier uses
|
|
123
|
+
observation health (any ABLATED → ESCALATE, any DEGRADED → RETRY, else HOLD).
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
name: str
|
|
127
|
+
parser: Parser
|
|
128
|
+
advance_when: Optional[StagePredicate] = None
|
|
129
|
+
retry_when: Optional[StagePredicate] = None
|
|
130
|
+
escalate_when: Optional[StagePredicate] = None
|
|
131
|
+
rollback_when: Optional[StagePredicate] = None
|
|
132
|
+
window: int = 20
|
|
133
|
+
|
|
134
|
+
def create_monitor(self) -> Monitor:
|
|
135
|
+
"""Fresh Monitor bound to this stage's parser and window."""
|
|
136
|
+
return Monitor(self.parser, window=self.window)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# -----------------------------------------------------------------------
|
|
140
|
+
# FlowResult
|
|
141
|
+
# -----------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class FlowResult:
|
|
145
|
+
"""Returned from Flow.observe() and flow advancement helpers."""
|
|
146
|
+
stage: str
|
|
147
|
+
stage_index: int
|
|
148
|
+
total_stages: int
|
|
149
|
+
expression: Optional[Expression]
|
|
150
|
+
guidance: Guidance
|
|
151
|
+
reason: str
|
|
152
|
+
|
|
153
|
+
def to_atom(self) -> str:
|
|
154
|
+
"""
|
|
155
|
+
Compact flow notation.
|
|
156
|
+
|
|
157
|
+
"flow:fermentation:lag_phase[2/5] → ADVANCE"
|
|
158
|
+
"flow:fermentation:COMPLETE[5/5]"
|
|
159
|
+
"""
|
|
160
|
+
if self.guidance == Guidance.COMPLETE:
|
|
161
|
+
return f"flow:{self.stage}[{self.total_stages}/{self.total_stages}]"
|
|
162
|
+
return (
|
|
163
|
+
f"flow:{self.stage}[{self.stage_index + 1}/{self.total_stages}]"
|
|
164
|
+
f" → {self.guidance.value}"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def to_dict(self) -> dict:
|
|
168
|
+
return {
|
|
169
|
+
"stage": self.stage,
|
|
170
|
+
"stage_index": self.stage_index,
|
|
171
|
+
"total_stages": self.total_stages,
|
|
172
|
+
"guidance": self.guidance.value,
|
|
173
|
+
"reason": self.reason,
|
|
174
|
+
"expression": self.expression.to_dict() if self.expression else None,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
def __repr__(self) -> str:
|
|
178
|
+
return f"FlowResult({self.to_atom()} — {self.reason})"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# -----------------------------------------------------------------------
|
|
182
|
+
# Flow
|
|
183
|
+
# -----------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
class Flow:
|
|
186
|
+
"""
|
|
187
|
+
An ordered sequence of stages with typed per-step guidance.
|
|
188
|
+
|
|
189
|
+
Each stage has its own Monitor, created fresh when the flow enters the
|
|
190
|
+
stage. Observations are routed to the current stage only; earlier-stage
|
|
191
|
+
state is preserved in the transition log.
|
|
192
|
+
|
|
193
|
+
Flow does not auto-advance. ``observe()`` returns guidance; the caller
|
|
194
|
+
(or a policy layer above) decides whether to call ``advance()``. This
|
|
195
|
+
keeps the flow deterministic and side-effect free, which matters for
|
|
196
|
+
replay, backtesting, and safety-critical use cases.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(
|
|
200
|
+
self,
|
|
201
|
+
stages: List[Stage],
|
|
202
|
+
label: str = "",
|
|
203
|
+
):
|
|
204
|
+
if not stages:
|
|
205
|
+
raise ValueError("Flow requires at least one stage")
|
|
206
|
+
names = [s.name for s in stages]
|
|
207
|
+
if len(set(names)) != len(names):
|
|
208
|
+
raise ValueError(f"Flow stages must have unique names, got {names}")
|
|
209
|
+
self.stages: List[Stage] = list(stages)
|
|
210
|
+
self.label = label
|
|
211
|
+
self._index = 0
|
|
212
|
+
self._monitors: Dict[str, Monitor] = {
|
|
213
|
+
stages[0].name: stages[0].create_monitor()
|
|
214
|
+
}
|
|
215
|
+
self._transitions: List[dict] = []
|
|
216
|
+
self._entered_at: datetime = datetime.now()
|
|
217
|
+
|
|
218
|
+
# ------------------------------------------------------------------
|
|
219
|
+
# Inspection
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def current(self) -> Optional[Stage]:
|
|
224
|
+
"""Current stage, or None if the flow is complete."""
|
|
225
|
+
if self.is_complete:
|
|
226
|
+
return None
|
|
227
|
+
return self.stages[self._index]
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def current_monitor(self) -> Optional[Monitor]:
|
|
231
|
+
"""Monitor for the current stage, or None if complete."""
|
|
232
|
+
if self.is_complete:
|
|
233
|
+
return None
|
|
234
|
+
return self._monitors[self.current.name]
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def is_complete(self) -> bool:
|
|
238
|
+
return self._index >= len(self.stages)
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def stage_index(self) -> int:
|
|
242
|
+
return self._index
|
|
243
|
+
|
|
244
|
+
@property
|
|
245
|
+
def transitions(self) -> List[dict]:
|
|
246
|
+
"""History of stage transitions."""
|
|
247
|
+
return list(self._transitions)
|
|
248
|
+
|
|
249
|
+
# ------------------------------------------------------------------
|
|
250
|
+
# Observation
|
|
251
|
+
# ------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
def observe(self, values: Dict[str, float], **kw) -> FlowResult:
|
|
254
|
+
"""
|
|
255
|
+
Route an observation to the current stage and return guidance.
|
|
256
|
+
|
|
257
|
+
``values`` and any kwargs are forwarded to ``Monitor.update()``, so
|
|
258
|
+
confidences, absences, and provenance all pass through.
|
|
259
|
+
"""
|
|
260
|
+
if self.is_complete:
|
|
261
|
+
return FlowResult(
|
|
262
|
+
stage=self.label or "flow",
|
|
263
|
+
stage_index=len(self.stages),
|
|
264
|
+
total_stages=len(self.stages),
|
|
265
|
+
expression=None,
|
|
266
|
+
guidance=Guidance.COMPLETE,
|
|
267
|
+
reason="all stages complete",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
stage = self.current
|
|
271
|
+
monitor = self._monitors[stage.name]
|
|
272
|
+
expr = monitor.update(values, **kw)
|
|
273
|
+
guidance, reason = self._classify(stage, expr)
|
|
274
|
+
|
|
275
|
+
return FlowResult(
|
|
276
|
+
stage=stage.name,
|
|
277
|
+
stage_index=self._index,
|
|
278
|
+
total_stages=len(self.stages),
|
|
279
|
+
expression=expr,
|
|
280
|
+
guidance=guidance,
|
|
281
|
+
reason=reason,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def _classify(self, stage: Stage, expr: Expression) -> tuple[Guidance, str]:
|
|
285
|
+
"""Evaluate stage predicates in severity order."""
|
|
286
|
+
# Highest severity first — a true escalate dominates an advance
|
|
287
|
+
if stage.escalate_when is not None and stage.escalate_when(expr):
|
|
288
|
+
return Guidance.ESCALATE, "escalate_when fired"
|
|
289
|
+
if stage.rollback_when is not None and stage.rollback_when(expr):
|
|
290
|
+
return Guidance.ROLLBACK, "rollback_when fired"
|
|
291
|
+
if stage.advance_when is not None and stage.advance_when(expr):
|
|
292
|
+
return Guidance.ADVANCE, "advance_when fired"
|
|
293
|
+
if stage.retry_when is not None and stage.retry_when(expr):
|
|
294
|
+
return Guidance.RETRY, "retry_when fired"
|
|
295
|
+
|
|
296
|
+
# Fallback classification using observation health alone
|
|
297
|
+
if any(o.health == Health.ABLATED for o in expr.observations):
|
|
298
|
+
return Guidance.ESCALATE, "component ABLATED"
|
|
299
|
+
if any(o.health == Health.DEGRADED for o in expr.observations):
|
|
300
|
+
return Guidance.RETRY, "component DEGRADED"
|
|
301
|
+
return Guidance.HOLD, "healthy, exit criteria not met"
|
|
302
|
+
|
|
303
|
+
# ------------------------------------------------------------------
|
|
304
|
+
# Stage transitions
|
|
305
|
+
# ------------------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
def advance(self) -> FlowResult:
|
|
308
|
+
"""
|
|
309
|
+
Move to the next stage. Creates a fresh Monitor for that stage.
|
|
310
|
+
Raises if the flow is already complete.
|
|
311
|
+
"""
|
|
312
|
+
if self.is_complete:
|
|
313
|
+
raise ValueError("flow is already complete")
|
|
314
|
+
prev = self.stages[self._index].name
|
|
315
|
+
self._index += 1
|
|
316
|
+
self._transitions.append({
|
|
317
|
+
"from": prev,
|
|
318
|
+
"to": self.stages[self._index].name if not self.is_complete else "(complete)",
|
|
319
|
+
"kind": "advance",
|
|
320
|
+
"at": datetime.now().isoformat(),
|
|
321
|
+
})
|
|
322
|
+
if not self.is_complete:
|
|
323
|
+
self._monitors[self.current.name] = self.current.create_monitor()
|
|
324
|
+
return self._status_result("advanced")
|
|
325
|
+
|
|
326
|
+
def rollback(self) -> FlowResult:
|
|
327
|
+
"""
|
|
328
|
+
Return to the previous stage. Creates a fresh Monitor for that stage
|
|
329
|
+
(prior observations during the rollback target are discarded).
|
|
330
|
+
Raises if already at the first stage.
|
|
331
|
+
"""
|
|
332
|
+
if self._index == 0:
|
|
333
|
+
raise ValueError("cannot rollback from first stage")
|
|
334
|
+
prev = self.current.name if not self.is_complete else "(complete)"
|
|
335
|
+
self._index -= 1
|
|
336
|
+
self._monitors[self.current.name] = self.current.create_monitor()
|
|
337
|
+
self._transitions.append({
|
|
338
|
+
"from": prev,
|
|
339
|
+
"to": self.current.name,
|
|
340
|
+
"kind": "rollback",
|
|
341
|
+
"at": datetime.now().isoformat(),
|
|
342
|
+
})
|
|
343
|
+
return self._status_result("rolled back")
|
|
344
|
+
|
|
345
|
+
def reset(self) -> None:
|
|
346
|
+
"""Reset to the first stage. Clears all monitors and transitions."""
|
|
347
|
+
self._index = 0
|
|
348
|
+
self._monitors = {self.stages[0].name: self.stages[0].create_monitor()}
|
|
349
|
+
self._transitions = []
|
|
350
|
+
self._entered_at = datetime.now()
|
|
351
|
+
|
|
352
|
+
def _status_result(self, reason: str) -> FlowResult:
|
|
353
|
+
if self.is_complete:
|
|
354
|
+
return FlowResult(
|
|
355
|
+
stage=self.label or "flow",
|
|
356
|
+
stage_index=len(self.stages),
|
|
357
|
+
total_stages=len(self.stages),
|
|
358
|
+
expression=None,
|
|
359
|
+
guidance=Guidance.COMPLETE,
|
|
360
|
+
reason=reason,
|
|
361
|
+
)
|
|
362
|
+
return FlowResult(
|
|
363
|
+
stage=self.current.name,
|
|
364
|
+
stage_index=self._index,
|
|
365
|
+
total_stages=len(self.stages),
|
|
366
|
+
expression=None,
|
|
367
|
+
guidance=Guidance.HOLD,
|
|
368
|
+
reason=reason,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# ------------------------------------------------------------------
|
|
372
|
+
# Serialization
|
|
373
|
+
# ------------------------------------------------------------------
|
|
374
|
+
|
|
375
|
+
def to_dict(self) -> dict:
|
|
376
|
+
"""Serialize the flow's current position and transition log."""
|
|
377
|
+
return {
|
|
378
|
+
"label": self.label,
|
|
379
|
+
"stages": [s.name for s in self.stages],
|
|
380
|
+
"index": self._index,
|
|
381
|
+
"transitions": list(self._transitions),
|
|
382
|
+
"entered_at": self._entered_at.isoformat(),
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
def __repr__(self) -> str:
|
|
386
|
+
if self.is_complete:
|
|
387
|
+
pos = f"COMPLETE[{len(self.stages)}/{len(self.stages)}]"
|
|
388
|
+
else:
|
|
389
|
+
pos = f"{self.current.name}[{self._index + 1}/{len(self.stages)}]"
|
|
390
|
+
return f"Flow(label={self.label!r}, {pos})"
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: margin
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.28
|
|
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
|
|
7
|
-
Project-URL: Homepage, https://
|
|
8
|
-
Project-URL: Repository, https://github.com/
|
|
9
|
-
Project-URL: Issues, https://github.com/
|
|
10
|
-
Project-URL: Changelog, https://github.com/
|
|
11
|
-
Project-URL: Documentation, https://github.com/
|
|
7
|
+
Project-URL: Homepage, https://copelabs.dev
|
|
8
|
+
Project-URL: Repository, https://github.com/Cope-Labs/margin
|
|
9
|
+
Project-URL: Issues, https://github.com/Cope-Labs/margin/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Cope-Labs/margin/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Documentation, https://github.com/Cope-Labs/margin#readme
|
|
12
12
|
Keywords: health,monitoring,uncertainty,observability,typed,classification,threshold,polarity,correction,audit,ledger,policy
|
|
13
13
|
Classifier: Development Status :: 4 - Beta
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
@@ -156,24 +156,24 @@ Ready-to-use threshold profiles for specific domains:
|
|
|
156
156
|
|
|
157
157
|
| Adapter | What it monitors | Polarity |
|
|
158
158
|
|---|---|---|
|
|
159
|
-
| [**healthcare**](https://github.com/
|
|
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
160
|
| [**godot**](https://github.com/sethc5/margin-godot) | Game systems (food, morale, stress, stamina) — native GDScript | mixed |
|
|
161
|
-
| [**homeassistant**](https://github.com/
|
|
162
|
-
| [**evcharging**](https://github.com/
|
|
163
|
-
| [**infrastructure**](https://github.com/
|
|
164
|
-
| [**aquarium**](https://github.com/
|
|
165
|
-
| [**greenhouse**](https://github.com/
|
|
166
|
-
| [**fitness**](https://github.com/
|
|
167
|
-
| [**transformer**](https://github.com/
|
|
168
|
-
| [**fastapi**](https://github.com/
|
|
169
|
-
| [**database**](https://github.com/
|
|
170
|
-
| [**celery**](https://github.com/
|
|
171
|
-
| [**dataframe**](https://github.com/
|
|
172
|
-
| [**pytest**](https://github.com/
|
|
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 |
|
|
173
173
|
|
|
174
174
|
## Docs
|
|
175
175
|
|
|
176
|
-
Full specification: [margin-language.md](https://github.com/
|
|
176
|
+
Full specification: [margin-language.md](https://github.com/Cope-Labs/margin/blob/main/margin/margin-language.md)
|
|
177
177
|
|
|
178
178
|
## License
|
|
179
179
|
|
|
@@ -19,6 +19,7 @@ margin/diff.py
|
|
|
19
19
|
margin/drift.py
|
|
20
20
|
margin/events.py
|
|
21
21
|
margin/fingerprint.py
|
|
22
|
+
margin/flow.py
|
|
22
23
|
margin/forecast.py
|
|
23
24
|
margin/health.py
|
|
24
25
|
margin/intent.py
|
|
@@ -60,6 +61,7 @@ tests/test_drift.py
|
|
|
60
61
|
tests/test_events.py
|
|
61
62
|
tests/test_fingerprint_ext.py
|
|
62
63
|
tests/test_float_tolerance.py
|
|
64
|
+
tests/test_flow.py
|
|
63
65
|
tests/test_forecast.py
|
|
64
66
|
tests/test_health.py
|
|
65
67
|
tests/test_healthcare.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "margin"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.28"
|
|
8
8
|
description = "Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -30,11 +30,11 @@ classifiers = [
|
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[project.urls]
|
|
33
|
-
Homepage = "https://
|
|
34
|
-
Repository = "https://github.com/
|
|
35
|
-
Issues = "https://github.com/
|
|
36
|
-
Changelog = "https://github.com/
|
|
37
|
-
Documentation = "https://github.com/
|
|
33
|
+
Homepage = "https://copelabs.dev"
|
|
34
|
+
Repository = "https://github.com/Cope-Labs/margin"
|
|
35
|
+
Issues = "https://github.com/Cope-Labs/margin/issues"
|
|
36
|
+
Changelog = "https://github.com/Cope-Labs/margin/blob/main/CHANGELOG.md"
|
|
37
|
+
Documentation = "https://github.com/Cope-Labs/margin#readme"
|
|
38
38
|
|
|
39
39
|
[tool.setuptools.packages.find]
|
|
40
40
|
include = ["margin*"]
|