margin 0.8.0__tar.gz → 0.8.2__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 (86) hide show
  1. {margin-0.8.0/margin.egg-info → margin-0.8.2}/PKG-INFO +1 -1
  2. {margin-0.8.0 → margin-0.8.2}/margin/__init__.py +6 -3
  3. {margin-0.8.0 → margin-0.8.2}/margin/causal.py +25 -1
  4. {margin-0.8.0 → margin-0.8.2}/margin/correlate.py +1 -0
  5. {margin-0.8.0 → margin-0.8.2}/margin/ledger.py +8 -0
  6. margin-0.8.2/margin/provenance.py +130 -0
  7. {margin-0.8.0 → margin-0.8.2/margin.egg-info}/PKG-INFO +1 -1
  8. {margin-0.8.0 → margin-0.8.2}/pyproject.toml +1 -1
  9. margin-0.8.0/margin/provenance.py +0 -21
  10. {margin-0.8.0 → margin-0.8.2}/LICENSE +0 -0
  11. {margin-0.8.0 → margin-0.8.2}/NOTICE +0 -0
  12. {margin-0.8.0 → margin-0.8.2}/README.md +0 -0
  13. {margin-0.8.0 → margin-0.8.2}/margin/__main__.py +0 -0
  14. {margin-0.8.0 → margin-0.8.2}/margin/algebra.py +0 -0
  15. {margin-0.8.0 → margin-0.8.2}/margin/anomaly.py +0 -0
  16. {margin-0.8.0 → margin-0.8.2}/margin/bridge.py +0 -0
  17. {margin-0.8.0 → margin-0.8.2}/margin/calibrate.py +0 -0
  18. {margin-0.8.0 → margin-0.8.2}/margin/composite.py +0 -0
  19. {margin-0.8.0 → margin-0.8.2}/margin/confidence.py +0 -0
  20. {margin-0.8.0 → margin-0.8.2}/margin/config.py +0 -0
  21. {margin-0.8.0 → margin-0.8.2}/margin/contract.py +0 -0
  22. {margin-0.8.0 → margin-0.8.2}/margin/diff.py +0 -0
  23. {margin-0.8.0 → margin-0.8.2}/margin/drift.py +0 -0
  24. {margin-0.8.0 → margin-0.8.2}/margin/events.py +0 -0
  25. {margin-0.8.0 → margin-0.8.2}/margin/forecast.py +0 -0
  26. {margin-0.8.0 → margin-0.8.2}/margin/health.py +0 -0
  27. {margin-0.8.0 → margin-0.8.2}/margin/intent.py +0 -0
  28. {margin-0.8.0 → margin-0.8.2}/margin/loop.py +0 -0
  29. {margin-0.8.0 → margin-0.8.2}/margin/observation.py +0 -0
  30. {margin-0.8.0 → margin-0.8.2}/margin/persist.py +0 -0
  31. {margin-0.8.0 → margin-0.8.2}/margin/policy/__init__.py +0 -0
  32. {margin-0.8.0 → margin-0.8.2}/margin/policy/compose.py +0 -0
  33. {margin-0.8.0 → margin-0.8.2}/margin/policy/core.py +0 -0
  34. {margin-0.8.0 → margin-0.8.2}/margin/policy/temporal.py +0 -0
  35. {margin-0.8.0 → margin-0.8.2}/margin/policy/trace.py +0 -0
  36. {margin-0.8.0 → margin-0.8.2}/margin/policy/tuning.py +0 -0
  37. {margin-0.8.0 → margin-0.8.2}/margin/policy/validate.py +0 -0
  38. {margin-0.8.0 → margin-0.8.2}/margin/predicates.py +0 -0
  39. {margin-0.8.0 → margin-0.8.2}/margin/streaming.py +0 -0
  40. {margin-0.8.0 → margin-0.8.2}/margin/transitions.py +0 -0
  41. {margin-0.8.0 → margin-0.8.2}/margin/uncertain.py +0 -0
  42. {margin-0.8.0 → margin-0.8.2}/margin/validity.py +0 -0
  43. {margin-0.8.0 → margin-0.8.2}/margin.egg-info/SOURCES.txt +0 -0
  44. {margin-0.8.0 → margin-0.8.2}/margin.egg-info/dependency_links.txt +0 -0
  45. {margin-0.8.0 → margin-0.8.2}/margin.egg-info/top_level.txt +0 -0
  46. {margin-0.8.0 → margin-0.8.2}/setup.cfg +0 -0
  47. {margin-0.8.0 → margin-0.8.2}/tests/test_algebra.py +0 -0
  48. {margin-0.8.0 → margin-0.8.2}/tests/test_anomaly.py +0 -0
  49. {margin-0.8.0 → margin-0.8.2}/tests/test_bridge.py +0 -0
  50. {margin-0.8.0 → margin-0.8.2}/tests/test_calibrate.py +0 -0
  51. {margin-0.8.0 → margin-0.8.2}/tests/test_causal.py +0 -0
  52. {margin-0.8.0 → margin-0.8.2}/tests/test_composite.py +0 -0
  53. {margin-0.8.0 → margin-0.8.2}/tests/test_confidence.py +0 -0
  54. {margin-0.8.0 → margin-0.8.2}/tests/test_config.py +0 -0
  55. {margin-0.8.0 → margin-0.8.2}/tests/test_contract.py +0 -0
  56. {margin-0.8.0 → margin-0.8.2}/tests/test_correlate.py +0 -0
  57. {margin-0.8.0 → margin-0.8.2}/tests/test_diff.py +0 -0
  58. {margin-0.8.0 → margin-0.8.2}/tests/test_drift.py +0 -0
  59. {margin-0.8.0 → margin-0.8.2}/tests/test_events.py +0 -0
  60. {margin-0.8.0 → margin-0.8.2}/tests/test_forecast.py +0 -0
  61. {margin-0.8.0 → margin-0.8.2}/tests/test_health.py +0 -0
  62. {margin-0.8.0 → margin-0.8.2}/tests/test_healthcare.py +0 -0
  63. {margin-0.8.0 → margin-0.8.2}/tests/test_intent.py +0 -0
  64. {margin-0.8.0 → margin-0.8.2}/tests/test_ledger.py +0 -0
  65. {margin-0.8.0 → margin-0.8.2}/tests/test_loop.py +0 -0
  66. {margin-0.8.0 → margin-0.8.2}/tests/test_neuro.py +0 -0
  67. {margin-0.8.0 → margin-0.8.2}/tests/test_observation.py +0 -0
  68. {margin-0.8.0 → margin-0.8.2}/tests/test_persist.py +0 -0
  69. {margin-0.8.0 → margin-0.8.2}/tests/test_policy.py +0 -0
  70. {margin-0.8.0 → margin-0.8.2}/tests/test_policy_compose.py +0 -0
  71. {margin-0.8.0 → margin-0.8.2}/tests/test_policy_temporal.py +0 -0
  72. {margin-0.8.0 → margin-0.8.2}/tests/test_policy_trace.py +0 -0
  73. {margin-0.8.0 → margin-0.8.2}/tests/test_policy_tuning.py +0 -0
  74. {margin-0.8.0 → margin-0.8.2}/tests/test_policy_validate.py +0 -0
  75. {margin-0.8.0 → margin-0.8.2}/tests/test_predicates.py +0 -0
  76. {margin-0.8.0 → margin-0.8.2}/tests/test_printer3d.py +0 -0
  77. {margin-0.8.0 → margin-0.8.2}/tests/test_provenance.py +0 -0
  78. {margin-0.8.0 → margin-0.8.2}/tests/test_reverse_bridge.py +0 -0
  79. {margin-0.8.0 → margin-0.8.2}/tests/test_ros2.py +0 -0
  80. {margin-0.8.0 → margin-0.8.2}/tests/test_staleness.py +0 -0
  81. {margin-0.8.0 → margin-0.8.2}/tests/test_streaming.py +0 -0
  82. {margin-0.8.0 → margin-0.8.2}/tests/test_transformer.py +0 -0
  83. {margin-0.8.0 → margin-0.8.2}/tests/test_transitions.py +0 -0
  84. {margin-0.8.0 → margin-0.8.2}/tests/test_uncertain.py +0 -0
  85. {margin-0.8.0 → margin-0.8.2}/tests/test_validity.py +0 -0
  86. {margin-0.8.0 → margin-0.8.2}/tests/test_windowing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.8.0
3
+ Version: 0.8.2
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
@@ -17,7 +17,10 @@ Structure:
17
17
  # Foundation
18
18
  from .confidence import Confidence
19
19
  from .validity import Validity, ValidityMode
20
- from .provenance import new_id, are_correlated, merge
20
+ from .provenance import (
21
+ new_id, _new_id, are_correlated, merge, merge_provenance,
22
+ ProvenanceNode, ProvenanceGraph, create_root_provenance,
23
+ )
21
24
  from .uncertain import UncertainValue, Source, EpistemicSource, UncertaintyMode
22
25
  from .algebra import add, subtract, multiply, divide, scale, compare, weighted_average
23
26
  from .health import Health, Thresholds, classify, SEVERITY
@@ -112,7 +115,8 @@ __all__ = [
112
115
  # Foundation
113
116
  "Confidence",
114
117
  "Validity", "ValidityMode",
115
- "new_id", "are_correlated", "merge",
118
+ "new_id", "_new_id", "are_correlated", "merge", "merge_provenance",
119
+ "ProvenanceNode", "ProvenanceGraph", "create_root_provenance",
116
120
  "UncertainValue", "Source",
117
121
  "add", "subtract", "multiply", "divide", "scale", "compare", "weighted_average",
118
122
  "Health", "Thresholds", "classify", "SEVERITY",
@@ -195,4 +199,3 @@ CorrectionRecord = Record
195
199
  CorrectionLedger = Ledger
196
200
  classify_gap_health = classify
197
201
  compare_to_threshold = compare
198
- merge_provenance = merge
@@ -45,6 +45,10 @@ class CausalLink:
45
45
  strength: 0.0-1.0, how strong the relationship is (1.0 = deterministic)
46
46
  condition: optional — this link only applies when source is in this health state
47
47
  evidence: free text — why we believe this link exists
48
+ origin: "asserted" (manually declared) or "discovered" (auto_causal_graph).
49
+ Asserted links are authoritative. Discovered links are correlational
50
+ heuristics — lag-based causal direction is plausible but not proven.
51
+ Never treat discovered links as ground truth in high-stakes domains.
48
52
  """
49
53
  source: str
50
54
  target: str
@@ -52,6 +56,15 @@ class CausalLink:
52
56
  strength: float = 1.0
53
57
  condition: Optional[Health] = None
54
58
  evidence: str = ""
59
+ origin: str = "asserted" # "asserted" | "discovered"
60
+
61
+ @property
62
+ def is_asserted(self) -> bool:
63
+ return self.origin == "asserted"
64
+
65
+ @property
66
+ def is_discovered(self) -> bool:
67
+ return self.origin == "discovered"
55
68
 
56
69
  def to_dict(self) -> dict:
57
70
  d = {
@@ -59,6 +72,7 @@ class CausalLink:
59
72
  "cause_type": self.cause_type.value,
60
73
  "strength": round(self.strength, 4),
61
74
  "evidence": self.evidence,
75
+ "origin": self.origin,
62
76
  }
63
77
  if self.condition:
64
78
  d["condition"] = self.condition.value
@@ -72,11 +86,13 @@ class CausalLink:
72
86
  strength=d.get("strength", 1.0),
73
87
  condition=Health(d["condition"]) if "condition" in d else None,
74
88
  evidence=d.get("evidence", ""),
89
+ origin=d.get("origin", "asserted"),
75
90
  )
76
91
 
77
92
  def __repr__(self) -> str:
78
93
  cond = f" when {self.condition.value}" if self.condition else ""
79
- return f"{self.source} --{self.cause_type.value}--> {self.target}{cond}"
94
+ tag = "[discovered]" if self.is_discovered else ""
95
+ return f"{self.source} --{self.cause_type.value}--> {self.target}{cond}{tag}"
80
96
 
81
97
 
82
98
  # -----------------------------------------------------------------------
@@ -124,6 +140,14 @@ class CausalGraph:
124
140
  """All links where `component` is the source (what it affects)."""
125
141
  return [l for l in self.links if l.source == component]
126
142
 
143
+ def asserted_links(self) -> list[CausalLink]:
144
+ """Links that were manually declared (authoritative)."""
145
+ return [l for l in self.links if l.is_asserted]
146
+
147
+ def discovered_links(self) -> list[CausalLink]:
148
+ """Links auto-discovered via correlation (heuristic, not proven)."""
149
+ return [l for l in self.links if l.is_discovered]
150
+
127
151
  def upstream(self, component: str, depth: int = 10) -> list[str]:
128
152
  """All components upstream of `component` (transitive causes)."""
129
153
  visited = set()
@@ -85,6 +85,7 @@ class Correlation:
85
85
  cause_type=cause_type,
86
86
  strength=self.strength,
87
87
  evidence=f"auto-correlated: r={self.coefficient:.3f}, lag={self.lag}, n={self.n_samples}",
88
+ origin="discovered", # lag-based causal direction is a heuristic, not proven
88
89
  )
89
90
 
90
91
  def to_dict(self) -> dict:
@@ -109,12 +109,20 @@ class Ledger:
109
109
  """
110
110
  Accumulates Records across a session.
111
111
  Provides aggregate stats and renders the intervention history.
112
+
113
+ max_records: optional bound on records kept. When set, old records
114
+ are dropped (oldest first) on each append. Use this for
115
+ long-running production loops to prevent unbounded growth.
116
+ None (default) = unbounded, suitable for batch/replay use.
112
117
  """
113
118
  label: str = ""
114
119
  records: list[Record] = field(default_factory=list)
120
+ max_records: Optional[int] = None
115
121
 
116
122
  def append(self, record: Record) -> None:
117
123
  self.records.append(record)
124
+ if self.max_records is not None and len(self.records) > self.max_records:
125
+ self.records.pop(0)
118
126
 
119
127
  def __len__(self) -> int:
120
128
  return len(self.records)
@@ -0,0 +1,130 @@
1
+ """
2
+ Provenance tracking for uncertain values.
3
+ Detects correlation between derived values via shared ancestry.
4
+ Optionally tracks full lineage via ProvenanceGraph.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import uuid
10
+ from dataclasses import dataclass, field
11
+ from typing import Optional
12
+
13
+
14
+ def new_id() -> str:
15
+ """Short unique ID for provenance tracking."""
16
+ return uuid.uuid4().hex[:8]
17
+
18
+ # margin-poc compat alias
19
+ _new_id = new_id
20
+
21
+
22
+ def are_correlated(ids_a: list[str], ids_b: list[str]) -> bool:
23
+ """True if two provenance chains share any common ancestor."""
24
+ return bool(set(ids_a) & set(ids_b))
25
+
26
+
27
+ def merge(ids_a: list[str], ids_b: list[str]) -> list[str]:
28
+ """Merge two provenance chains, adding a new derived ID."""
29
+ return list(set(ids_a) | set(ids_b) | {new_id()})
30
+
31
+ # margin-poc compat alias
32
+ merge_provenance = merge
33
+
34
+
35
+ # -----------------------------------------------------------------------
36
+ # Provenance graph — full lineage tracking
37
+ # -----------------------------------------------------------------------
38
+
39
+ @dataclass
40
+ class ProvenanceNode:
41
+ """
42
+ A node in the provenance graph representing a single derived value.
43
+
44
+ id: unique identifier
45
+ operation: what created this value (e.g. "add", "measure_gap_IOI")
46
+ source_ids: IDs of input values this was derived from
47
+ """
48
+ id: str
49
+ operation: str
50
+ source_ids: list[str] = field(default_factory=list)
51
+
52
+ def to_dict(self) -> dict:
53
+ return {"id": self.id, "operation": self.operation, "source_ids": self.source_ids}
54
+
55
+ @classmethod
56
+ def from_dict(cls, d: dict) -> ProvenanceNode:
57
+ return cls(id=d["id"], operation=d["operation"], source_ids=d.get("source_ids", []))
58
+
59
+
60
+ class ProvenanceGraph:
61
+ """
62
+ Tracks the full lineage of uncertain values through computations.
63
+
64
+ Used to detect correlations (shared provenance affects how
65
+ uncertainties combine) and to explain where a value came from.
66
+ """
67
+
68
+ def __init__(self):
69
+ self.nodes: dict[str, ProvenanceNode] = {}
70
+
71
+ def add_node(self, node: ProvenanceNode) -> None:
72
+ self.nodes[node.id] = node
73
+
74
+ def get_node(self, node_id: str) -> Optional[ProvenanceNode]:
75
+ return self.nodes.get(node_id)
76
+
77
+ def find_common_ancestors(self, ids_a: set[str], ids_b: set[str]) -> set[str]:
78
+ """Find common ancestor IDs between two provenance sets."""
79
+ return ids_a & ids_b
80
+
81
+ def trace_lineage(self, node_id: str, depth: int = 10) -> list[ProvenanceNode]:
82
+ """Trace the lineage of a node back through the graph (oldest first)."""
83
+ lineage = []
84
+ to_visit = [node_id]
85
+ visited: set[str] = set()
86
+
87
+ while to_visit and len(lineage) < depth:
88
+ current_id = to_visit.pop(0)
89
+ if current_id in visited:
90
+ continue
91
+ visited.add(current_id)
92
+
93
+ node = self.get_node(current_id)
94
+ if node is None:
95
+ continue
96
+
97
+ lineage.append(node)
98
+ to_visit.extend(node.source_ids)
99
+
100
+ return list(reversed(lineage))
101
+
102
+ def create_root(self, operation: str = "root") -> str:
103
+ """Create a root provenance node and return its ID."""
104
+ node_id = new_id()
105
+ self.add_node(ProvenanceNode(id=node_id, operation=operation))
106
+ return node_id
107
+
108
+ def derive(self, operation: str, source_ids: list[str]) -> str:
109
+ """Create a derived provenance node and return its ID."""
110
+ node_id = new_id()
111
+ self.add_node(ProvenanceNode(id=node_id, operation=operation, source_ids=source_ids))
112
+ return node_id
113
+
114
+ def to_dict(self) -> dict:
115
+ return {"nodes": {k: v.to_dict() for k, v in self.nodes.items()}}
116
+
117
+ @classmethod
118
+ def from_dict(cls, d: dict) -> ProvenanceGraph:
119
+ g = cls()
120
+ for k, v in d.get("nodes", {}).items():
121
+ g.add_node(ProvenanceNode.from_dict(v))
122
+ return g
123
+
124
+ def __repr__(self) -> str:
125
+ return f"ProvenanceGraph({len(self.nodes)} nodes)"
126
+
127
+
128
+ def create_root_provenance(operation: str = "root") -> list[str]:
129
+ """Create a fresh provenance ID for a root (measured) value."""
130
+ return [new_id()]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: margin
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: Typed health classification, uncertainty algebra, and correction auditing for any system that measures things and needs to explain what happened.
5
5
  Author: Cope Labs LLC
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "margin"
7
- version = "0.8.0"
7
+ version = "0.8.2"
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"}
@@ -1,21 +0,0 @@
1
- """
2
- Provenance tracking for uncertain values.
3
- Detects correlation between derived values via shared ancestry.
4
- """
5
-
6
- import uuid
7
-
8
-
9
- def new_id() -> str:
10
- """Short unique ID for provenance tracking."""
11
- return uuid.uuid4().hex[:8]
12
-
13
-
14
- def are_correlated(ids_a: list[str], ids_b: list[str]) -> bool:
15
- """True if two provenance chains share any common ancestor."""
16
- return bool(set(ids_a) & set(ids_b))
17
-
18
-
19
- def merge(ids_a: list[str], ids_b: list[str]) -> list[str]:
20
- """Merge two provenance chains, adding a new derived ID."""
21
- return list(set(ids_a) | set(ids_b) | {new_id()})
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes