madcop 0.1.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.
madcop-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lin Ruihan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
madcop-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,201 @@
1
+ Metadata-Version: 2.4
2
+ Name: madcop
3
+ Version: 0.1.0
4
+ Summary: madcop — the supply chain cop that goes mad for anomalies. Pluggable LangGraph framework: detect, diagnose, decide.
5
+ Author-email: Lin Ruihan <chuiniu@me.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/linmy666/madcop
8
+ Project-URL: Repository, https://github.com/linmy666/madcop
9
+ Project-URL: Issues, https://github.com/linmy666/madcop/issues
10
+ Keywords: supply-chain,langgraph,agent,anomaly-detection,rca,root-cause-analysis,ai-pm
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Office/Business
23
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: rich>=13.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=7.0; extra == "dev"
30
+ Requires-Dist: build>=1.0; extra == "dev"
31
+ Requires-Dist: twine>=4.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # madcop
35
+
36
+ > **mad** + **cop** — the supply chain cop that goes *mad* for anomalies.
37
+ > Pluggable LangGraph framework: from "detect" to "diagnose" to "decide", with self-evolution.
38
+
39
+ [![Tests](https://img.shields.io/badge/tests-45%20passing-brightgreen)](#tests)
40
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](#requirements)
41
+ [![License](https://img.shields.io/badge/license-MIT-lightgrey)](#license)
42
+ [![TestPyPI](https://img.shields.io/badge/TestPyPI-v0.1.0-blue)](https://test.pypi.org/project/madcop/)
43
+
44
+ ## What is madcop?
45
+
46
+ **madcop** is a pluggable framework that turns raw supply chain telemetry
47
+ (orders, shipments, warehouse readings, contracts) into **decision prompts**
48
+ with full causal chains. Where most tools stop at "alert fired", madcop walks
49
+ the chain back to the **human decision** that made the anomaly possible.
50
+
51
+ The name is short for **mad cop** — a cop that goes mad for anomalies. Not
52
+ in a punitive sense, but in the sense of "won't let a single anomaly go
53
+ untraced to its source."
54
+
55
+ ## Why?
56
+
57
+ A typical supply chain alert reads:
58
+
59
+ > ⚠️ Cold-chain temperature exceeded threshold at 14:30.
60
+
61
+ That tells you **what** happened, not **why** it could happen. madcop answers
62
+ the second question:
63
+
64
+ > The temperature breach on SHIP-2026-0615-CG-SH traces back to a BD
65
+ > decision (DEC-2026-03-12-N3) made three months earlier to accept the
66
+ > supplier's "fine equals exemption" concession. That decision shaped
67
+ > CLAUSE-04 — a **passive** clause that punishes the breach but does not
68
+ > prevent it. The contract was signed with 冷链速运 at Q1 cost-cutting
69
+ > pressure, and the shipment is now exposed to the same failure mode.
70
+
71
+ The PM framing: an alert without a cause is a notification. An alert with a
72
+ cause is a **decision prompt**. madcop's job is to bridge the two.
73
+
74
+ ## Architecture (4 layers, 1 graph)
75
+
76
+ ```
77
+ ┌──────────────────────────────────────────────────────┐
78
+ │ L4 Strategy Router — zero-code YAML policies │
79
+ ├──────────────────────────────────────────────────────┤
80
+ │ L3 LangGraph — detect → diagnose → decide │
81
+ │ → learn (state machine) │
82
+ ├──────────────────────────────────────────────────────┤
83
+ │ L2 Anomaly Engine — rules · RCA · counterfactual│
84
+ ├──────────────────────────────────────────────────────┤
85
+ │ L1 Unified Data Layer — OMS/TMS/WMS/BMS adapters │
86
+ └──────────────────────────────────────────────────────┘
87
+ ```
88
+
89
+ **L1 — Unified Data Layer** (`madcop/event.py`, `madcop/adapters/`)
90
+ A `UnifiedEvent` is the lingua franca. Every adapter implements `BaseAdapter`
91
+ and yields events with frozen, UTC-validated, severity-rated fields.
92
+
93
+ **L2 — Anomaly Engine** (`madcop/anomaly/`, `madcop/rca/`)
94
+ 5 shipped rules + a `Detector` that orchestrates them. RCA walks a typed
95
+ property graph from any finding back to a decision.
96
+
97
+ **L3 — LangGraph Orchestrator** (`madcop/graph/`) — *planned, W5*
98
+ A typed state machine that sequences detect → diagnose → decide → learn.
99
+
100
+ **L4 — Strategy Router** (`madcop/strategy/`) — *planned, W7*
101
+ YAML policies + feedback-weighted registry. Self-evolution is real here:
102
+ weekly reports roll up the week's findings, and policies are ranked by
103
+ their rolling effectiveness.
104
+
105
+ ## What's shipped today (W1 + W2 + W3)
106
+
107
+ | Layer | Component | Status |
108
+ |-------|-----------|--------|
109
+ | L1 | `UnifiedEvent` with UTC + severity + source/event_type validation | ✅ |
110
+ | L1 | `BaseAdapter` contract + WMS mock (cold-chain) | ✅ |
111
+ | L2 | `Detector` + 5 rules (cold-chain temp / sustained / OMS cancel / TMS lead / BMS score) | ✅ |
112
+ | L2 | `KnowledgeGraph` + `trace()` + `explain()` RCA | ✅ |
113
+ | L2 | Cold-chain seed graph (5 nodes, 4 edges) | ✅ |
114
+ | L4 | Strategy registry, weekly report, LLM backend | 🔜 W7 |
115
+
116
+ ## Installation
117
+
118
+ ```bash
119
+ pip install madcop
120
+ ```
121
+
122
+ Or from TestPyPI (the current published version):
123
+
124
+ ```bash
125
+ pip install --index-url https://test.pypi.org/simple/ madcop
126
+ ```
127
+
128
+ ## Quick start
129
+
130
+ ```bash
131
+ # W1: see the raw event stream
132
+ python -m madcop run coldchain
133
+
134
+ # W2: detect anomalies
135
+ python -m madcop run anomalies
136
+
137
+ # W3: detect + trace each finding to a root cause (the headline feature)
138
+ python -m madcop run rca
139
+ ```
140
+
141
+ ### What `run rca` looks like
142
+
143
+ ```
144
+ madcop RCA demo — 3 finding(s) on SHIP-2026-0615-CG-SH
145
+
146
+ ━━━ finding 1/3 ━━━
147
+ rule: wms.coldchain.temperature_breach
148
+ summary: Cold-chain temperature -14.2°C exceeds threshold -15.0°C by 0.8°C
149
+ chain: 5 step(s), root cause:
150
+ ╭──────────────────────────────── root cause ────────────────────────────────╮
151
+ │ decision DEC-2026-03-12-N3 (BD 接受乙方'罚款即免责'让步) (by BD-Lin) — │
152
+ │ rationale: Q1 降本压力 → shaped clause CLAUSE-04 (温控异常通知条款) PASSIVE │
153
+ │ ("温控异常时, 承运商应在 30 分钟内书面通知甲方, 逾期每日扣 0.5% 服务费") → │
154
+ │ under contract CONT-2026-0312 (冷链速运 / 2026 年度框架) → carried by │
155
+ │ 冷链速运 → on shipment SHIP-2026-0615-CG-SH (广州→上海, 冷链, 2026-06-15) │
156
+ ╰──────────────────────────────────────────────────────────────────────────────╯
157
+ ```
158
+
159
+ ## Tests
160
+
161
+ ```bash
162
+ pip install -e ".[dev]"
163
+ pytest
164
+ ```
165
+
166
+ **45 tests, all passing.** They cover the L1 contract (UTC validation, event
167
+ type / source system consistency, adapter behavior), the L2 detector (every
168
+ rule, plus state-machine semantics for windowed rules), and the RCA graph
169
+ (forward/reverse traversal, empty chain, unknown subject).
170
+
171
+ ## Roadmap
172
+
173
+ See [`ROADMAP.md`](ROADMAP.md). 8 weeks, 1 commit per week. Current: **W3
174
+ done, ready to push**.
175
+
176
+ ## Requirements
177
+
178
+ - Python 3.10+
179
+ - `rich >= 13.0`
180
+
181
+ ## Project status
182
+
183
+ Alpha. The architecture is real, the data layer is real, the anomaly rules
184
+ are real, and the RCA traces are real. The adapters are mock data today;
185
+ real wire integrations to OMS / TMS / WMS / BMS systems are scoped for
186
+ later (see Roadmap).
187
+
188
+ ## License
189
+
190
+ MIT. See [`LICENSE`](LICENSE).
191
+
192
+ ## Why "madcop"?
193
+
194
+ When the user asked for a name for "the agent that goes mad for anomalies",
195
+ the obvious answer was **mad + cop**. The product is a cop that goes mad for
196
+ anomalies — not in a punitive sense, but in the sense of "won't let a
197
+ single anomaly go untraced to its source."
198
+
199
+ ## Contact
200
+
201
+ Lin Ruihan · chuiniu@me.com
madcop-0.1.0/README.md ADDED
@@ -0,0 +1,168 @@
1
+ # madcop
2
+
3
+ > **mad** + **cop** — the supply chain cop that goes *mad* for anomalies.
4
+ > Pluggable LangGraph framework: from "detect" to "diagnose" to "decide", with self-evolution.
5
+
6
+ [![Tests](https://img.shields.io/badge/tests-45%20passing-brightgreen)](#tests)
7
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](#requirements)
8
+ [![License](https://img.shields.io/badge/license-MIT-lightgrey)](#license)
9
+ [![TestPyPI](https://img.shields.io/badge/TestPyPI-v0.1.0-blue)](https://test.pypi.org/project/madcop/)
10
+
11
+ ## What is madcop?
12
+
13
+ **madcop** is a pluggable framework that turns raw supply chain telemetry
14
+ (orders, shipments, warehouse readings, contracts) into **decision prompts**
15
+ with full causal chains. Where most tools stop at "alert fired", madcop walks
16
+ the chain back to the **human decision** that made the anomaly possible.
17
+
18
+ The name is short for **mad cop** — a cop that goes mad for anomalies. Not
19
+ in a punitive sense, but in the sense of "won't let a single anomaly go
20
+ untraced to its source."
21
+
22
+ ## Why?
23
+
24
+ A typical supply chain alert reads:
25
+
26
+ > ⚠️ Cold-chain temperature exceeded threshold at 14:30.
27
+
28
+ That tells you **what** happened, not **why** it could happen. madcop answers
29
+ the second question:
30
+
31
+ > The temperature breach on SHIP-2026-0615-CG-SH traces back to a BD
32
+ > decision (DEC-2026-03-12-N3) made three months earlier to accept the
33
+ > supplier's "fine equals exemption" concession. That decision shaped
34
+ > CLAUSE-04 — a **passive** clause that punishes the breach but does not
35
+ > prevent it. The contract was signed with 冷链速运 at Q1 cost-cutting
36
+ > pressure, and the shipment is now exposed to the same failure mode.
37
+
38
+ The PM framing: an alert without a cause is a notification. An alert with a
39
+ cause is a **decision prompt**. madcop's job is to bridge the two.
40
+
41
+ ## Architecture (4 layers, 1 graph)
42
+
43
+ ```
44
+ ┌──────────────────────────────────────────────────────┐
45
+ │ L4 Strategy Router — zero-code YAML policies │
46
+ ├──────────────────────────────────────────────────────┤
47
+ │ L3 LangGraph — detect → diagnose → decide │
48
+ │ → learn (state machine) │
49
+ ├──────────────────────────────────────────────────────┤
50
+ │ L2 Anomaly Engine — rules · RCA · counterfactual│
51
+ ├──────────────────────────────────────────────────────┤
52
+ │ L1 Unified Data Layer — OMS/TMS/WMS/BMS adapters │
53
+ └──────────────────────────────────────────────────────┘
54
+ ```
55
+
56
+ **L1 — Unified Data Layer** (`madcop/event.py`, `madcop/adapters/`)
57
+ A `UnifiedEvent` is the lingua franca. Every adapter implements `BaseAdapter`
58
+ and yields events with frozen, UTC-validated, severity-rated fields.
59
+
60
+ **L2 — Anomaly Engine** (`madcop/anomaly/`, `madcop/rca/`)
61
+ 5 shipped rules + a `Detector` that orchestrates them. RCA walks a typed
62
+ property graph from any finding back to a decision.
63
+
64
+ **L3 — LangGraph Orchestrator** (`madcop/graph/`) — *planned, W5*
65
+ A typed state machine that sequences detect → diagnose → decide → learn.
66
+
67
+ **L4 — Strategy Router** (`madcop/strategy/`) — *planned, W7*
68
+ YAML policies + feedback-weighted registry. Self-evolution is real here:
69
+ weekly reports roll up the week's findings, and policies are ranked by
70
+ their rolling effectiveness.
71
+
72
+ ## What's shipped today (W1 + W2 + W3)
73
+
74
+ | Layer | Component | Status |
75
+ |-------|-----------|--------|
76
+ | L1 | `UnifiedEvent` with UTC + severity + source/event_type validation | ✅ |
77
+ | L1 | `BaseAdapter` contract + WMS mock (cold-chain) | ✅ |
78
+ | L2 | `Detector` + 5 rules (cold-chain temp / sustained / OMS cancel / TMS lead / BMS score) | ✅ |
79
+ | L2 | `KnowledgeGraph` + `trace()` + `explain()` RCA | ✅ |
80
+ | L2 | Cold-chain seed graph (5 nodes, 4 edges) | ✅ |
81
+ | L4 | Strategy registry, weekly report, LLM backend | 🔜 W7 |
82
+
83
+ ## Installation
84
+
85
+ ```bash
86
+ pip install madcop
87
+ ```
88
+
89
+ Or from TestPyPI (the current published version):
90
+
91
+ ```bash
92
+ pip install --index-url https://test.pypi.org/simple/ madcop
93
+ ```
94
+
95
+ ## Quick start
96
+
97
+ ```bash
98
+ # W1: see the raw event stream
99
+ python -m madcop run coldchain
100
+
101
+ # W2: detect anomalies
102
+ python -m madcop run anomalies
103
+
104
+ # W3: detect + trace each finding to a root cause (the headline feature)
105
+ python -m madcop run rca
106
+ ```
107
+
108
+ ### What `run rca` looks like
109
+
110
+ ```
111
+ madcop RCA demo — 3 finding(s) on SHIP-2026-0615-CG-SH
112
+
113
+ ━━━ finding 1/3 ━━━
114
+ rule: wms.coldchain.temperature_breach
115
+ summary: Cold-chain temperature -14.2°C exceeds threshold -15.0°C by 0.8°C
116
+ chain: 5 step(s), root cause:
117
+ ╭──────────────────────────────── root cause ────────────────────────────────╮
118
+ │ decision DEC-2026-03-12-N3 (BD 接受乙方'罚款即免责'让步) (by BD-Lin) — │
119
+ │ rationale: Q1 降本压力 → shaped clause CLAUSE-04 (温控异常通知条款) PASSIVE │
120
+ │ ("温控异常时, 承运商应在 30 分钟内书面通知甲方, 逾期每日扣 0.5% 服务费") → │
121
+ │ under contract CONT-2026-0312 (冷链速运 / 2026 年度框架) → carried by │
122
+ │ 冷链速运 → on shipment SHIP-2026-0615-CG-SH (广州→上海, 冷链, 2026-06-15) │
123
+ ╰──────────────────────────────────────────────────────────────────────────────╯
124
+ ```
125
+
126
+ ## Tests
127
+
128
+ ```bash
129
+ pip install -e ".[dev]"
130
+ pytest
131
+ ```
132
+
133
+ **45 tests, all passing.** They cover the L1 contract (UTC validation, event
134
+ type / source system consistency, adapter behavior), the L2 detector (every
135
+ rule, plus state-machine semantics for windowed rules), and the RCA graph
136
+ (forward/reverse traversal, empty chain, unknown subject).
137
+
138
+ ## Roadmap
139
+
140
+ See [`ROADMAP.md`](ROADMAP.md). 8 weeks, 1 commit per week. Current: **W3
141
+ done, ready to push**.
142
+
143
+ ## Requirements
144
+
145
+ - Python 3.10+
146
+ - `rich >= 13.0`
147
+
148
+ ## Project status
149
+
150
+ Alpha. The architecture is real, the data layer is real, the anomaly rules
151
+ are real, and the RCA traces are real. The adapters are mock data today;
152
+ real wire integrations to OMS / TMS / WMS / BMS systems are scoped for
153
+ later (see Roadmap).
154
+
155
+ ## License
156
+
157
+ MIT. See [`LICENSE`](LICENSE).
158
+
159
+ ## Why "madcop"?
160
+
161
+ When the user asked for a name for "the agent that goes mad for anomalies",
162
+ the obvious answer was **mad + cop**. The product is a cop that goes mad for
163
+ anomalies — not in a punitive sense, but in the sense of "won't let a
164
+ single anomaly go untraced to its source."
165
+
166
+ ## Contact
167
+
168
+ Lin Ruihan · chuiniu@me.com
@@ -0,0 +1,3 @@
1
+ """madcop: a pluggable LangGraph framework for supply chain anomaly orchestration."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,123 @@
1
+ """madcop CLI entry point.
2
+
3
+ Two demo scenarios today:
4
+ python -m madcop run coldchain # W1 — print the event stream
5
+ python -m madcop run anomalies coldchain # W2 — run anomaly detection
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import sys
12
+
13
+ from .adapters.wms import WMSAdapter
14
+ from .anomaly.rules import default_detector
15
+ from .rca.graph import explain, trace
16
+ from .rca.seed import build_coldchain_seed
17
+
18
+
19
+ def _format_event(idx: int, total: int, ev) -> str:
20
+ ts = ev.parsed_timestamp.strftime("%H:%M:%S")
21
+ val = f"{ev.value:>6.1f}°C" if ev.value is not None else " — "
22
+ sev = "·" * ev.severity
23
+ return f" [{idx:>2}/{total}] {ts} {val} sev{ev.severity} {sev} {ev.attributes.get('note', '')}"
24
+
25
+
26
+ def run_coldchain() -> int:
27
+ """W1 demo: print the WMS cold-chain event stream."""
28
+ adapter = WMSAdapter()
29
+ events = sorted(adapter.fetch(), key=lambda e: e.parsed_timestamp)
30
+ if not events:
31
+ print("(no events)", file=sys.stderr)
32
+ return 1
33
+ print(f"cold-chain timeline for {events[0].subject_id}")
34
+ print(f" threshold: {adapter.COLD_CHAIN_THRESHOLD_C}°C")
35
+ print()
36
+ for i, ev in enumerate(events, 1):
37
+ print(_format_event(i, len(events), ev))
38
+ breaches = [e for e in events if e.value is not None and e.value > adapter.COLD_CHAIN_THRESHOLD_C]
39
+ print()
40
+ print(f" → {len(breaches)} threshold breach(es) detected")
41
+ return 0
42
+
43
+
44
+ def run_anomalies_coldchain() -> int:
45
+ """W2 demo: run all 5 anomaly rules on the cold-chain stream."""
46
+ adapter = WMSAdapter()
47
+ events = sorted(adapter.fetch(), key=lambda e: e.parsed_timestamp)
48
+ detector = default_detector()
49
+ findings = list(detector.run(events))
50
+ print(f"madcop anomaly report — subject={events[0].subject_id if events else 'N/A'}")
51
+ print(f" events: {len(events)} rules: {len(detector.rules)} findings: {len(findings)}")
52
+ print()
53
+ if not findings:
54
+ print(" (no anomalies)")
55
+ return 0
56
+ for i, f in enumerate(findings, 1):
57
+ print(f" [{i}/{len(findings)}] sev{f.severity} {f.rule_id}")
58
+ print(f" {f.summary}")
59
+ return 0
60
+
61
+
62
+ def run_rca_coldchain() -> int:
63
+ """W3 demo: detect anomalies and trace each to a root-cause decision."""
64
+ from rich.console import Console
65
+ from rich.panel import Panel
66
+
67
+ console = Console()
68
+ events = sorted(WMSAdapter().fetch(), key=lambda e: e.parsed_timestamp)
69
+ findings = list(default_detector().run(events))
70
+ g = build_coldchain_seed()
71
+
72
+ console.print(f"[bold]madcop RCA demo[/] — {len(findings)} finding(s) on {events[0].subject_id}\n")
73
+ if not findings:
74
+ console.print(" [dim](no findings to trace)[/]")
75
+ return 0
76
+ for i, f in enumerate(findings, 1):
77
+ console.print(f"[cyan]━━━ finding {i}/{len(findings)} ━━━[/]")
78
+ console.print(f" rule: [yellow]{f.rule_id}[/]")
79
+ console.print(f" summary: {f.summary}")
80
+ chain = trace(f, g)
81
+ if not chain.steps:
82
+ console.print(" [dim](no causal chain — subject not in knowledge graph)[/]")
83
+ continue
84
+ console.print(f" chain: [bold]{len(chain.steps)}[/] step(s), root cause:")
85
+ console.print(Panel(explain(chain), title="root cause", border_style="red"))
86
+ return 0
87
+
88
+
89
+ def main(argv: list[str] | None = None) -> int:
90
+ parser = argparse.ArgumentParser(
91
+ prog="madcop",
92
+ description="madcop — the supply chain cop that goes mad for anomalies.",
93
+ )
94
+ sub = parser.add_subparsers(dest="cmd", required=True)
95
+
96
+ # run <scenario>
97
+ run_p = sub.add_parser("run", help="Run a scenario")
98
+ run_sub = run_p.add_subparsers(dest="scenario", required=True)
99
+ run_sub.add_parser("coldchain", help="W1: print the cold-chain event stream")
100
+ run_sub.add_parser("anomalies", help="W2: run anomaly detection on the cold-chain stream")
101
+ run_sub.add_parser("rca", help="W3: detect anomalies and trace each to a root cause")
102
+
103
+ # demo <scenario> — alias for `run`
104
+ demo_p = sub.add_parser("demo", help="Alias for `run`")
105
+ demo_sub = demo_p.add_subparsers(dest="scenario", required=True)
106
+ demo_sub.add_parser("coldchain", help="W1: print the cold-chain event stream")
107
+ demo_sub.add_parser("anomalies", help="W2: run anomaly detection on the cold-chain stream")
108
+ demo_sub.add_parser("rca", help="W3: detect anomalies and trace each to a root cause")
109
+
110
+ args = parser.parse_args(argv)
111
+ if args.cmd in ("run", "demo"):
112
+ if args.scenario == "coldchain":
113
+ return run_coldchain()
114
+ if args.scenario == "anomalies":
115
+ return run_anomalies_coldchain()
116
+ if args.scenario == "rca":
117
+ return run_rca_coldchain()
118
+ parser.print_help()
119
+ return 2
120
+
121
+
122
+ if __name__ == "__main__":
123
+ sys.exit(main())
File without changes
@@ -0,0 +1,82 @@
1
+ """Adapter contract — the seam between the outside world and `UnifiedEvent`.
2
+
3
+ Every system (OMS / TMS / WMS / BMS / future) ships an adapter that implements
4
+ `BaseAdapter`. Two responsibilities, and only two:
5
+
6
+ 1. **Pull** raw data from a system (HTTP, DB, file, etc.) and yield `UnifiedEvent`s.
7
+ 2. **Push** actions back to the system (e.g. re-route shipment, mark exception).
8
+
9
+ We deliberately keep the adapter small. Logic that *reads* the event stream
10
+ lives in the anomaly engine. Logic that *acts* on decisions lives in the
11
+ strategy router. The adapter is just a translator.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from abc import ABC, abstractmethod
17
+ from typing import Iterator
18
+
19
+ from ..event import SourceSystem, UnifiedEvent
20
+
21
+
22
+ class BaseAdapter(ABC):
23
+ """The contract every system adapter must satisfy.
24
+
25
+ Adapters are stateful objects (they hold a connection, an API key, a
26
+ file handle). They are constructed once per session and called repeatedly.
27
+ """
28
+
29
+ source_system: SourceSystem # set by subclass
30
+
31
+ @abstractmethod
32
+ def fetch(self, *, since: str | None = None, subject_id: str | None = None) -> Iterator[UnifiedEvent]:
33
+ """Yield events from the upstream system, optionally filtered.
34
+
35
+ `since` is a UTC ISO 8601 string. If given, only events with
36
+ `timestamp >= since` should be returned.
37
+
38
+ `subject_id`, if given, restricts to events about a single business
39
+ object (an order, a SKU, a shipment, a contract). Adapters that
40
+ cannot filter server-side should filter in-memory after fetching.
41
+
42
+ Yields events in **any order** — the LangGraph orchestrator sorts
43
+ by timestamp.
44
+ """
45
+ raise NotImplementedError
46
+
47
+ @abstractmethod
48
+ def execute(self, action: "Action") -> dict:
49
+ """Push an action back to the system. Returns the system's response.
50
+
51
+ See `Action` for the schema. Adapters that cannot perform an action
52
+ (e.g. WMS can't change a contract) should raise
53
+ `UnsupportedActionError`.
54
+ """
55
+ raise NotImplementedError
56
+
57
+ def health_check(self) -> bool:
58
+ """Optional. Default: assume healthy. Override for real wire checks."""
59
+ return True
60
+
61
+
62
+ class UnsupportedActionError(NotImplementedError):
63
+ """Raised when an adapter is asked to perform an action outside its scope."""
64
+
65
+
66
+ from dataclasses import dataclass
67
+ from typing import Any, Mapping
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class Action:
72
+ """A command the strategy router wants executed against an adapter.
73
+
74
+ The action is **adapter-agnostic**: the router does not know which system
75
+ will run it. The router picks an adapter based on `target_system` and the
76
+ adapter translates the rest.
77
+ """
78
+
79
+ target_system: SourceSystem
80
+ action_type: str # free-form, adapter-defined vocabulary
81
+ subject_id: str # what the action is about
82
+ parameters: Mapping[str, Any] # adapter-specific args