macverdict 0.2.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.
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ dist/
4
+ build/
5
+ .venv/
6
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anirudh Jayaraman
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.
@@ -0,0 +1,227 @@
1
+ Metadata-Version: 2.5
2
+ Name: macverdict
3
+ Version: 0.2.0
4
+ Summary: Stop watching graphs: macverdict tells you why your Mac was slow this week and what to do about it. Slowdown blame, RAM verdict, battery forecast and MacBook Air throttling, from quiet background sampling.
5
+ Project-URL: Homepage, https://github.com/anirudhjayaraman/macverdict
6
+ Project-URL: Source, https://github.com/anirudhjayaraman/macverdict
7
+ Project-URL: Issues, https://github.com/anirudhjayaraman/macverdict/issues
8
+ Author: Anirudh Jayaraman
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: battery,diagnostics,launchd,macbook,macos,monitoring,performance
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Topic :: System :: Monitoring
19
+ Requires-Python: >=3.9
20
+ Requires-Dist: psutil>=5.9
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # macverdict
26
+
27
+ **Stop watching graphs. macverdict tells you why your Mac was slow this week, and what to do about it.**
28
+
29
+ *Formerly called macpulse. If you installed it under that name, run `macverdict setup` once: it brings your history across and removes the old parts.*
30
+
31
+ Activity Monitor, Stats and iStat Menus show you numbers *right now*, and leave the diagnosis to you.
32
+ macverdict quietly samples your Mac in the background (about one second every 15 minutes) and turns
33
+ weeks of history into four plain answers:
34
+
35
+ 1. **What's slowing me down?** It blames the app that was actually the biggest memory or CPU user
36
+ each time your Mac struggled, grouping helper processes so Chrome's 60 processes count as Chrome.
37
+ 2. **Do I need more RAM?** A verdict from weeks of memory pressure and swap, not the misleading
38
+ "memory used" figure, including what to buy next time.
39
+ 3. **How long will my battery last?** Your real wear rate vs. Apple's rating, and roughly when you'll
40
+ hit 80% capacity.
41
+ 4. **Is my Air overheating?** How often the fanless MacBook Air slowed itself down to cool off, and
42
+ which apps were running when it did.
43
+
44
+ ```
45
+ $ macverdict insights # illustration: three weeks on an 8 GB MacBook Air
46
+ macverdict insights · MacBook Air · 8.0 GB RAM · last 30 days
47
+
48
+ ● What's slowing you down
49
+ Google Chrome was the biggest memory user in 85% of your 794 slowdown moments.
50
+ · Google Chrome: the biggest memory user in 85% of slowdowns, typically 5.0 GB.
51
+ · Final Cut Pro: the biggest CPU user in 15% of slowdowns.
52
+ · Biggest win: quit or trim Google Chrome when your Mac feels slow.
53
+
54
+ ● Do you need more RAM?
55
+ 8 GB isn't enough for how you work. Your next Mac should have 16 GB.
56
+ · Memory pressure was elevated in 33% of samples.
57
+ · Swap: typically under 9.0 GB, peak 13.5 GB.
58
+
59
+ ● How long will your battery last?
60
+ At your pace, it reaches 80% capacity around May 2027.
61
+ · It's wearing faster than Apple's rating: 3.8% of capacity per 100 cycles, vs 2% expected.
62
+
63
+ ● Is your Air overheating?
64
+ Your Mac slowed itself down to cool off in 6% of samples.
65
+ · Busiest apps when it ran hot: Final Cut Pro.
66
+ ```
67
+
68
+ ## See it on a real Mac
69
+
70
+ A real week on a MacBook Air (Apple M5, 16 GB). `macverdict report` turns the history into a page
71
+ like this. **[Open the full sample report](https://anirudhjayaraman.github.io/macverdict/sample-report.html)**.
72
+
73
+ ![macverdict insights: Chrome behind every slowdown, 16 GB is enough, battery healthy, no throttling](https://raw.githubusercontent.com/anirudhjayaraman/macverdict/main/docs/insights.png)
74
+
75
+ The trends make the story visible: memory pressure (shaded) on Mon 19 and Tue 20, matching the
76
+ swap spike, then a quiet rest of the week.
77
+
78
+ ![macverdict trends: a week of health score, CPU, memory with pressure shaded, swap, disk, battery](https://raw.githubusercontent.com/anirudhjayaraman/macverdict/main/docs/trends.png)
79
+
80
+ Free and open source, one dependency (`psutil`), no sudo, no cloud: nothing leaves your Mac.
81
+ Built for Apple-silicon MacBooks, works on any Mac running macOS 12+.
82
+
83
+ ## How it compares
84
+
85
+ | | Live stats | Keeps history | Tells you *why* & what to do | Price |
86
+ |---|---|---|---|---|
87
+ | Activity Monitor (built in) | ✅ | ❌ | ❌ | Free |
88
+ | [Stats](https://github.com/exelban/stats) | ✅ menu bar | ❌ | ❌ | Free |
89
+ | [iStat Menus](https://bjango.com/mac/istatmenus/) | ✅ very detailed | Some | Rule-based alerts | Paid |
90
+ | [macmon](https://github.com/vladkens/macmon) / [mactop](https://github.com/anemll/mactop) | ✅ power & GPU | ❌ | ❌ | Free |
91
+ | [Glances](https://github.com/nicolargo/glances) | ✅ any OS | Short buffer / export | ❌ | Free |
92
+ | [Mole](https://github.com/tw93/mole) `mo status` | ✅ | ❌ | Live health score | Free |
93
+ | [coconutBattery](https://www.coconut-flavour.com/coconutbattery/) | Battery only | ✅ battery | ❌ | Free / paid |
94
+ | **macverdict** | Snapshot (`macverdict`) | ✅ everything | ✅ **blame, RAM verdict, battery forecast, throttling** | **Free** |
95
+
96
+ **Use macverdict alongside a live monitor, not instead of one.** If you want to watch GPU power or fan
97
+ speed in real time, Stats and macmon are excellent. macverdict answers the questions those tools can't:
98
+ what happened over the last few weeks, and what you should change.
99
+
100
+ ## Everyday health check
101
+
102
+ ```
103
+ $ macverdict
104
+ macverdict · Apple M5 · macOS 26.5.2 · 16.0 GB RAM · up 1d 17h
105
+
106
+ Health score: 100 / 100
107
+
108
+ OK cpu CPU 6%, load 1.9 on 10 cores.
109
+ OK memory Memory pressure normal, 66% used, swap 0 B.
110
+ OK disk 698.7 GB free (75%).
111
+ OK battery 52% charged, capacity 100%, 71 cycles, 30.6°C.
112
+ OK thermal No thermal throttling.
113
+ ```
114
+
115
+ ## What it checks
116
+
117
+ | Area | Signals | Why it matters |
118
+ |---|---|---|
119
+ | CPU | usage, 5-min load vs. core count, runaway processes | sustained load means work is queuing |
120
+ | Memory | **macOS memory pressure** (the real signal, not "% used"), swap, biggest apps | the #1 cause of slowdowns on 8–16 GB Macs |
121
+ | Disk | free space on the Data volume | macOS needs room for swap and updates |
122
+ | Battery | maximum capacity, cycle count, temperature | <80% capacity = service territory; heat = throttling |
123
+ | Thermal | macOS's own thermal state, plus CPU speed limit | fanless Airs slow themselves down when hot |
124
+ | Uptime | days since restart | long uptimes accumulate swap and leaks |
125
+
126
+ Each finding comes with plain-English advice, and everything rolls up into a 0–100 health score.
127
+
128
+ ## Install
129
+
130
+ ```bash
131
+ brew install anirudhjayaraman/tap/macverdict
132
+ macverdict setup
133
+ ```
134
+
135
+ That's it. `macverdict setup` starts background recording, builds the small alert app and sends a
136
+ test alert (click **Allow** if macOS asks). Then run `macverdict` any time.
137
+
138
+ To remove it: `macverdict uninstall`, then `brew uninstall macverdict`.
139
+
140
+ <details>
141
+ <summary>Not using Homebrew?</summary>
142
+
143
+ Install with [uv](https://docs.astral.sh/uv/) or pipx instead, then run `macverdict setup`:
144
+
145
+ ```bash
146
+ uv tool install git+https://github.com/anirudhjayaraman/macverdict
147
+ ```
148
+
149
+ </details>
150
+
151
+ Needs macOS 12 or later. Clickable alerts need Apple's Command Line Tools (`xcode-select --install`);
152
+ without them, everything else still works.
153
+
154
+ ## Use
155
+
156
+ ```bash
157
+ macverdict # diagnose right now
158
+ macverdict insights # blame, RAM verdict, battery forecast, throttling
159
+ macverdict report # HTML report with insights and trends, opens in your browser
160
+ macverdict history --days 30 # min/avg/max summary in the terminal
161
+ macverdict export history.csv # take your data to a spreadsheet or notebook
162
+ macverdict check --json # machine-readable snapshot
163
+ macverdict setup --every 5 # record every 5 minutes instead of 15
164
+ macverdict notify-test # check that alerts reach you
165
+ macverdict uninstall # stop background recording and remove the alert app
166
+ ```
167
+
168
+ The background agent shows up as **macverdict** in System Settings → General → Login Items &
169
+ Extensions (or as `python3` if the alert app couldn't be built).
170
+ macOS labels it "unidentified developer" because it's signed on your own Mac rather than with a
171
+ paid Apple Developer ID, which is normal for open-source command-line tools. Switching it off
172
+ there stops recording.
173
+
174
+ The background agent runs at low priority, survives reboots, and shows a macOS notification only
175
+ when something is critical (e.g. memory pressure critical, battery overheating, disk nearly full).
176
+
177
+ ## Notifications
178
+
179
+ The background agent only alerts you when something is critical, and clicking an alert opens your
180
+ macverdict report. `macverdict setup` builds the small alert app that makes this work.
181
+
182
+ It's built from source on your machine (`src/macverdict/notifier/MacverdictNotifier.swift`, about
183
+ 100 lines) and signed locally, so there's no downloaded binary and no Apple account involved.
184
+ It has no Dock icon and only runs for a few seconds when posting an alert or handling a click.
185
+
186
+ Without it, macverdict falls back to macOS's built-in `osascript`. Those alerts show as coming
187
+ from **Script Editor**, and clicking one opens Script Editor. If no banner appears at all, check
188
+ System Settings → Notifications (macverdict or Script Editor) and that no Focus mode is on.
189
+
190
+ ## Use it from Python
191
+
192
+ ```python
193
+ from macverdict import collect, evaluate, health_score, insights, storage
194
+
195
+ snap = collect()
196
+ findings = evaluate(snap)
197
+ print(health_score(findings), [f.message for f in findings if f.level != "ok"])
198
+
199
+ rows = storage.load(storage.connect(), days=30, full=True)
200
+ for i in insights.build(rows, snap["system"]["memory_total"], insights.hardware_model()):
201
+ print(i.title, "→", i.headline)
202
+ ```
203
+
204
+ ## Where data lives
205
+
206
+ `~/Library/Application Support/macverdict/` holds `history.db` (SQLite), `report.html` and the agent
207
+ logs. Samples older than 90 days are pruned automatically. Set `MACVERDICT_HOME` to use another folder.
208
+
209
+ ## How it works
210
+
211
+ - `psutil` for CPU, memory, swap, disk, network and processes
212
+ - `sysctl kern.memorystatus_vm_pressure_level` for macOS memory pressure
213
+ - `ioreg -rn AppleSmartBattery` for battery capacity, cycles and temperature
214
+ - `NSProcessInfo.thermalState` (read via ctypes, no PyObjC) and `pmset -g therm` for throttling
215
+ - a per-user `launchd` agent for scheduling
216
+ - a tiny Swift app (built locally) for clickable notifications, with `osascript` as a fallback
217
+
218
+ ## Development
219
+
220
+ ```bash
221
+ uv venv && uv pip install -e ".[dev]"
222
+ .venv/bin/pytest
223
+ ```
224
+
225
+ ## License
226
+
227
+ MIT
@@ -0,0 +1,203 @@
1
+ # macverdict
2
+
3
+ **Stop watching graphs. macverdict tells you why your Mac was slow this week, and what to do about it.**
4
+
5
+ *Formerly called macpulse. If you installed it under that name, run `macverdict setup` once: it brings your history across and removes the old parts.*
6
+
7
+ Activity Monitor, Stats and iStat Menus show you numbers *right now*, and leave the diagnosis to you.
8
+ macverdict quietly samples your Mac in the background (about one second every 15 minutes) and turns
9
+ weeks of history into four plain answers:
10
+
11
+ 1. **What's slowing me down?** It blames the app that was actually the biggest memory or CPU user
12
+ each time your Mac struggled, grouping helper processes so Chrome's 60 processes count as Chrome.
13
+ 2. **Do I need more RAM?** A verdict from weeks of memory pressure and swap, not the misleading
14
+ "memory used" figure, including what to buy next time.
15
+ 3. **How long will my battery last?** Your real wear rate vs. Apple's rating, and roughly when you'll
16
+ hit 80% capacity.
17
+ 4. **Is my Air overheating?** How often the fanless MacBook Air slowed itself down to cool off, and
18
+ which apps were running when it did.
19
+
20
+ ```
21
+ $ macverdict insights # illustration: three weeks on an 8 GB MacBook Air
22
+ macverdict insights · MacBook Air · 8.0 GB RAM · last 30 days
23
+
24
+ ● What's slowing you down
25
+ Google Chrome was the biggest memory user in 85% of your 794 slowdown moments.
26
+ · Google Chrome: the biggest memory user in 85% of slowdowns, typically 5.0 GB.
27
+ · Final Cut Pro: the biggest CPU user in 15% of slowdowns.
28
+ · Biggest win: quit or trim Google Chrome when your Mac feels slow.
29
+
30
+ ● Do you need more RAM?
31
+ 8 GB isn't enough for how you work. Your next Mac should have 16 GB.
32
+ · Memory pressure was elevated in 33% of samples.
33
+ · Swap: typically under 9.0 GB, peak 13.5 GB.
34
+
35
+ ● How long will your battery last?
36
+ At your pace, it reaches 80% capacity around May 2027.
37
+ · It's wearing faster than Apple's rating: 3.8% of capacity per 100 cycles, vs 2% expected.
38
+
39
+ ● Is your Air overheating?
40
+ Your Mac slowed itself down to cool off in 6% of samples.
41
+ · Busiest apps when it ran hot: Final Cut Pro.
42
+ ```
43
+
44
+ ## See it on a real Mac
45
+
46
+ A real week on a MacBook Air (Apple M5, 16 GB). `macverdict report` turns the history into a page
47
+ like this. **[Open the full sample report](https://anirudhjayaraman.github.io/macverdict/sample-report.html)**.
48
+
49
+ ![macverdict insights: Chrome behind every slowdown, 16 GB is enough, battery healthy, no throttling](https://raw.githubusercontent.com/anirudhjayaraman/macverdict/main/docs/insights.png)
50
+
51
+ The trends make the story visible: memory pressure (shaded) on Mon 19 and Tue 20, matching the
52
+ swap spike, then a quiet rest of the week.
53
+
54
+ ![macverdict trends: a week of health score, CPU, memory with pressure shaded, swap, disk, battery](https://raw.githubusercontent.com/anirudhjayaraman/macverdict/main/docs/trends.png)
55
+
56
+ Free and open source, one dependency (`psutil`), no sudo, no cloud: nothing leaves your Mac.
57
+ Built for Apple-silicon MacBooks, works on any Mac running macOS 12+.
58
+
59
+ ## How it compares
60
+
61
+ | | Live stats | Keeps history | Tells you *why* & what to do | Price |
62
+ |---|---|---|---|---|
63
+ | Activity Monitor (built in) | ✅ | ❌ | ❌ | Free |
64
+ | [Stats](https://github.com/exelban/stats) | ✅ menu bar | ❌ | ❌ | Free |
65
+ | [iStat Menus](https://bjango.com/mac/istatmenus/) | ✅ very detailed | Some | Rule-based alerts | Paid |
66
+ | [macmon](https://github.com/vladkens/macmon) / [mactop](https://github.com/anemll/mactop) | ✅ power & GPU | ❌ | ❌ | Free |
67
+ | [Glances](https://github.com/nicolargo/glances) | ✅ any OS | Short buffer / export | ❌ | Free |
68
+ | [Mole](https://github.com/tw93/mole) `mo status` | ✅ | ❌ | Live health score | Free |
69
+ | [coconutBattery](https://www.coconut-flavour.com/coconutbattery/) | Battery only | ✅ battery | ❌ | Free / paid |
70
+ | **macverdict** | Snapshot (`macverdict`) | ✅ everything | ✅ **blame, RAM verdict, battery forecast, throttling** | **Free** |
71
+
72
+ **Use macverdict alongside a live monitor, not instead of one.** If you want to watch GPU power or fan
73
+ speed in real time, Stats and macmon are excellent. macverdict answers the questions those tools can't:
74
+ what happened over the last few weeks, and what you should change.
75
+
76
+ ## Everyday health check
77
+
78
+ ```
79
+ $ macverdict
80
+ macverdict · Apple M5 · macOS 26.5.2 · 16.0 GB RAM · up 1d 17h
81
+
82
+ Health score: 100 / 100
83
+
84
+ OK cpu CPU 6%, load 1.9 on 10 cores.
85
+ OK memory Memory pressure normal, 66% used, swap 0 B.
86
+ OK disk 698.7 GB free (75%).
87
+ OK battery 52% charged, capacity 100%, 71 cycles, 30.6°C.
88
+ OK thermal No thermal throttling.
89
+ ```
90
+
91
+ ## What it checks
92
+
93
+ | Area | Signals | Why it matters |
94
+ |---|---|---|
95
+ | CPU | usage, 5-min load vs. core count, runaway processes | sustained load means work is queuing |
96
+ | Memory | **macOS memory pressure** (the real signal, not "% used"), swap, biggest apps | the #1 cause of slowdowns on 8–16 GB Macs |
97
+ | Disk | free space on the Data volume | macOS needs room for swap and updates |
98
+ | Battery | maximum capacity, cycle count, temperature | <80% capacity = service territory; heat = throttling |
99
+ | Thermal | macOS's own thermal state, plus CPU speed limit | fanless Airs slow themselves down when hot |
100
+ | Uptime | days since restart | long uptimes accumulate swap and leaks |
101
+
102
+ Each finding comes with plain-English advice, and everything rolls up into a 0–100 health score.
103
+
104
+ ## Install
105
+
106
+ ```bash
107
+ brew install anirudhjayaraman/tap/macverdict
108
+ macverdict setup
109
+ ```
110
+
111
+ That's it. `macverdict setup` starts background recording, builds the small alert app and sends a
112
+ test alert (click **Allow** if macOS asks). Then run `macverdict` any time.
113
+
114
+ To remove it: `macverdict uninstall`, then `brew uninstall macverdict`.
115
+
116
+ <details>
117
+ <summary>Not using Homebrew?</summary>
118
+
119
+ Install with [uv](https://docs.astral.sh/uv/) or pipx instead, then run `macverdict setup`:
120
+
121
+ ```bash
122
+ uv tool install git+https://github.com/anirudhjayaraman/macverdict
123
+ ```
124
+
125
+ </details>
126
+
127
+ Needs macOS 12 or later. Clickable alerts need Apple's Command Line Tools (`xcode-select --install`);
128
+ without them, everything else still works.
129
+
130
+ ## Use
131
+
132
+ ```bash
133
+ macverdict # diagnose right now
134
+ macverdict insights # blame, RAM verdict, battery forecast, throttling
135
+ macverdict report # HTML report with insights and trends, opens in your browser
136
+ macverdict history --days 30 # min/avg/max summary in the terminal
137
+ macverdict export history.csv # take your data to a spreadsheet or notebook
138
+ macverdict check --json # machine-readable snapshot
139
+ macverdict setup --every 5 # record every 5 minutes instead of 15
140
+ macverdict notify-test # check that alerts reach you
141
+ macverdict uninstall # stop background recording and remove the alert app
142
+ ```
143
+
144
+ The background agent shows up as **macverdict** in System Settings → General → Login Items &
145
+ Extensions (or as `python3` if the alert app couldn't be built).
146
+ macOS labels it "unidentified developer" because it's signed on your own Mac rather than with a
147
+ paid Apple Developer ID, which is normal for open-source command-line tools. Switching it off
148
+ there stops recording.
149
+
150
+ The background agent runs at low priority, survives reboots, and shows a macOS notification only
151
+ when something is critical (e.g. memory pressure critical, battery overheating, disk nearly full).
152
+
153
+ ## Notifications
154
+
155
+ The background agent only alerts you when something is critical, and clicking an alert opens your
156
+ macverdict report. `macverdict setup` builds the small alert app that makes this work.
157
+
158
+ It's built from source on your machine (`src/macverdict/notifier/MacverdictNotifier.swift`, about
159
+ 100 lines) and signed locally, so there's no downloaded binary and no Apple account involved.
160
+ It has no Dock icon and only runs for a few seconds when posting an alert or handling a click.
161
+
162
+ Without it, macverdict falls back to macOS's built-in `osascript`. Those alerts show as coming
163
+ from **Script Editor**, and clicking one opens Script Editor. If no banner appears at all, check
164
+ System Settings → Notifications (macverdict or Script Editor) and that no Focus mode is on.
165
+
166
+ ## Use it from Python
167
+
168
+ ```python
169
+ from macverdict import collect, evaluate, health_score, insights, storage
170
+
171
+ snap = collect()
172
+ findings = evaluate(snap)
173
+ print(health_score(findings), [f.message for f in findings if f.level != "ok"])
174
+
175
+ rows = storage.load(storage.connect(), days=30, full=True)
176
+ for i in insights.build(rows, snap["system"]["memory_total"], insights.hardware_model()):
177
+ print(i.title, "→", i.headline)
178
+ ```
179
+
180
+ ## Where data lives
181
+
182
+ `~/Library/Application Support/macverdict/` holds `history.db` (SQLite), `report.html` and the agent
183
+ logs. Samples older than 90 days are pruned automatically. Set `MACVERDICT_HOME` to use another folder.
184
+
185
+ ## How it works
186
+
187
+ - `psutil` for CPU, memory, swap, disk, network and processes
188
+ - `sysctl kern.memorystatus_vm_pressure_level` for macOS memory pressure
189
+ - `ioreg -rn AppleSmartBattery` for battery capacity, cycles and temperature
190
+ - `NSProcessInfo.thermalState` (read via ctypes, no PyObjC) and `pmset -g therm` for throttling
191
+ - a per-user `launchd` agent for scheduling
192
+ - a tiny Swift app (built locally) for clickable notifications, with `osascript` as a fallback
193
+
194
+ ## Development
195
+
196
+ ```bash
197
+ uv venv && uv pip install -e ".[dev]"
198
+ .venv/bin/pytest
199
+ ```
200
+
201
+ ## License
202
+
203
+ MIT
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "macverdict"
7
+ version = "0.2.0"
8
+ description = "Stop watching graphs: macverdict tells you why your Mac was slow this week and what to do about it. Slowdown blame, RAM verdict, battery forecast and MacBook Air throttling, from quiet background sampling."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Anirudh Jayaraman" }]
14
+ keywords = ["macos", "macbook", "monitoring", "diagnostics", "performance", "battery", "launchd"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Intended Audience :: End Users/Desktop",
19
+ "Intended Audience :: Developers",
20
+ "Operating System :: MacOS",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: System :: Monitoring",
23
+ ]
24
+ dependencies = ["psutil>=5.9"]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/anirudhjayaraman/macverdict"
28
+ Source = "https://github.com/anirudhjayaraman/macverdict"
29
+ Issues = "https://github.com/anirudhjayaraman/macverdict/issues"
30
+
31
+ [project.optional-dependencies]
32
+ dev = ["pytest>=7"]
33
+
34
+ [project.scripts]
35
+ macverdict = "macverdict.cli:main"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["src/macverdict"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
42
+
43
+ [tool.hatch.build.targets.sdist]
44
+ exclude = ["docs", "legacy"]
@@ -0,0 +1,8 @@
1
+ """macverdict: tells you why your Mac was slow, and what to do about it. (Formerly macpulse.)"""
2
+
3
+ __version__ = "0.2.0"
4
+
5
+ from .checks import Finding, evaluate, health_score
6
+ from .collectors import collect
7
+
8
+ __all__ = ["collect", "evaluate", "health_score", "Finding", "__version__"]
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,147 @@
1
+ """Turn a snapshot into findings with plain-English advice, and a 0-100 score."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass
6
+ from typing import Any
7
+
8
+ from .fmt import human_bytes
9
+
10
+ GB = 1024 ** 3
11
+ LEVELS = ("ok", "info", "warn", "critical")
12
+ PENALTY = {"ok": 0, "info": 2, "warn": 10, "critical": 25}
13
+
14
+
15
+ @dataclass
16
+ class Finding:
17
+ level: str # one of LEVELS
18
+ area: str
19
+ message: str
20
+ advice: str = ""
21
+
22
+ def to_dict(self) -> dict[str, Any]:
23
+ return asdict(self)
24
+
25
+
26
+ def _cpu(s: dict) -> list[Finding]:
27
+ cpu, cores = s["cpu"], s["system"]["cpu_count"] or 1
28
+ load_ratio = cpu["load_avg"][1] / cores
29
+ out = []
30
+ if cpu["percent"] >= 90:
31
+ out.append(Finding("warn", "cpu", f"CPU is busy at {cpu['percent']:.0f}%.",
32
+ "Check the top CPU processes below; quit anything you are not using."))
33
+ if load_ratio >= 1.5:
34
+ out.append(Finding("warn", "cpu", f"5-minute load is {cpu['load_avg'][1]:.1f} on {cores} cores.",
35
+ "Sustained load above core count means work is queuing up."))
36
+ if not out:
37
+ out.append(Finding("ok", "cpu", f"CPU {cpu['percent']:.0f}%, load {cpu['load_avg'][1]:.1f} on {cores} cores."))
38
+ for p in s.get("top_cpu", [])[:1]:
39
+ if p["cpu_percent"] >= 80:
40
+ out.append(Finding("info", "cpu", f"{p['name']} (pid {p['pid']}) is using {p['cpu_percent']:.0f}% CPU.",
41
+ "Expected if it is doing real work; otherwise it may be stuck."))
42
+ return out
43
+
44
+
45
+ def _memory(s: dict) -> list[Finding]:
46
+ m = s["memory"]
47
+ level = (m.get("pressure") or {}).get("level")
48
+ swap = m["swap_used"]
49
+ out = []
50
+ if level == "critical":
51
+ out.append(Finding("critical", "memory", "Memory pressure is CRITICAL.",
52
+ "macOS is compressing and swapping heavily. Close large apps or browser tabs."))
53
+ elif level == "warn":
54
+ out.append(Finding("warn", "memory", "Memory pressure is elevated.",
55
+ "Close apps you are not using; this is the main cause of slowdowns on 8-16 GB Macs."))
56
+ if swap >= 6 * GB:
57
+ out.append(Finding("critical", "memory", f"Swap in use: {human_bytes(swap)}.",
58
+ "Heavy swapping wears the SSD and slows everything. A restart clears it."))
59
+ elif swap >= 2 * GB:
60
+ out.append(Finding("warn", "memory", f"Swap in use: {human_bytes(swap)}.",
61
+ "Some swapping is normal, but sustained growth means you need more free RAM."))
62
+ if not out:
63
+ out.append(Finding("ok", "memory", f"Memory pressure {level or 'n/a'}, {m['percent']:.0f}% used, "
64
+ f"swap {human_bytes(swap)}."))
65
+ biggest = (s.get("apps") or s.get("top_memory") or [])[:1]
66
+ for p in biggest:
67
+ if p["rss"] >= 0.25 * m["total"]:
68
+ out.append(Finding("info", "memory", f"{p['name']} is using {human_bytes(p['rss'])} of RAM.",
69
+ "Restarting a long-running app often returns leaked memory."))
70
+ return out
71
+
72
+
73
+ def _disk(s: dict) -> list[Finding]:
74
+ d = s["disk"]
75
+ pct, free = d["free_percent"], d["free"]
76
+ if pct < 5 or free < 10 * GB:
77
+ return [Finding("critical", "disk", f"Only {human_bytes(free)} free ({pct:.0f}%).",
78
+ "macOS needs free space for swap and updates. Clear Downloads, caches, or old iOS backups.")]
79
+ if pct < 15:
80
+ return [Finding("warn", "disk", f"Disk is getting full: {human_bytes(free)} free ({pct:.0f}%).",
81
+ "Use System Settings > General > Storage to find large files.")]
82
+ return [Finding("ok", "disk", f"{human_bytes(free)} free ({pct:.0f}%).")]
83
+
84
+
85
+ def _battery(s: dict) -> list[Finding]:
86
+ b = s.get("battery")
87
+ if not b:
88
+ return []
89
+ out = []
90
+ health = b.get("max_capacity_percent") or b.get("health_percent")
91
+ if health is not None and health < 80:
92
+ out.append(Finding("warn", "battery", f"Battery maximum capacity is {health:.0f}%.",
93
+ "Apple considers batteries below 80% consumed; consider a service appointment."))
94
+ if (b.get("cycle_count") or 0) >= 1000:
95
+ out.append(Finding("info", "battery", f"Battery has {b['cycle_count']} cycles (rated for 1000).", ""))
96
+ temp = b.get("temperature_c")
97
+ if temp is not None and temp >= 45:
98
+ out.append(Finding("critical", "battery", f"Battery is hot: {temp:.0f}°C.",
99
+ "Move the Mac off soft surfaces and out of the sun; stop heavy work until it cools."))
100
+ elif temp is not None and temp >= 38:
101
+ out.append(Finding("warn", "battery", f"Battery is warm: {temp:.0f}°C.",
102
+ "The MacBook Air is fanless, so sustained heat leads to throttling."))
103
+ if not out:
104
+ out.append(Finding("ok", "battery", f"{b['percent']:.0f}% charged, capacity {health}%, "
105
+ f"{b.get('cycle_count')} cycles, {temp}°C."))
106
+ return out
107
+
108
+
109
+ def _thermal(s: dict) -> list[Finding]:
110
+ t = s["thermal"]
111
+ state = t.get("state")
112
+ if state == "critical":
113
+ return [Finding("critical", "thermal", "macOS reports CRITICAL heat and is slowing the Mac hard.",
114
+ "Stop heavy work now and give it airflow: hard surface, out of the sun.")]
115
+ if t["throttled"]:
116
+ detail = f"limited to {t['cpu_speed_limit']}% speed" if t["cpu_speed_limit"] < 100 else "thermal state is serious"
117
+ return [Finding("warn", "thermal", f"Your Mac is slowing itself down to cool off ({detail}).",
118
+ "Reduce load or improve airflow. On a fanless MacBook Air this is the main cause of "
119
+ "slowdowns during long heavy tasks.")]
120
+ if state == "fair":
121
+ return [Finding("info", "thermal", "Your Mac is warm (thermal state: fair).",
122
+ "Not slowing down yet, but sustained heavy work may start throttling.")]
123
+ return [Finding("ok", "thermal", "No thermal throttling.")]
124
+
125
+
126
+ def _uptime(s: dict) -> list[Finding]:
127
+ days = (s["timestamp"] - s["system"]["boot_time"]) / 86400
128
+ if days >= 14:
129
+ return [Finding("info", "uptime", f"Up for {days:.0f} days.",
130
+ "A restart clears swap, leaked memory and pending updates.")]
131
+ return []
132
+
133
+
134
+ RULES = (_cpu, _memory, _disk, _battery, _thermal, _uptime)
135
+
136
+
137
+ def evaluate(snapshot: dict) -> list[Finding]:
138
+ findings = [f for rule in RULES for f in rule(snapshot)]
139
+ return sorted(findings, key=lambda f: -LEVELS.index(f.level))
140
+
141
+
142
+ def health_score(findings: list[Finding]) -> int:
143
+ return max(0, 100 - sum(PENALTY[f.level] for f in findings))
144
+
145
+
146
+ def worst_level(findings: list[Finding]) -> str:
147
+ return max((f.level for f in findings), key=LEVELS.index, default="ok")