memorydetective 1.0.0 → 1.0.1
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.
- package/CHANGELOG.md +13 -1
- package/README.md +1 -1
- package/USAGE.md +259 -0
- package/dist/cli.js +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.0.1] — 2026-05-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `USAGE.md` walkthrough covering the three usage modes (CLI, `--json`, MCP), the 8 cycle patterns and their fix hints, the end-to-end flow of how fixes go from diagnosis to a code edit (memorydetective diagnoses; the LLM agent applies the edit using its own code-editing tools), common follow-up prompts, and troubleshooting. README links to it from the Quickstart pointer line.
|
|
14
|
+
- `USAGE.md` is included in the npm tarball (added to `package.json` `files` whitelist).
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- No code changes from `1.0.0` — this is a documentation bump.
|
|
19
|
+
|
|
9
20
|
## [1.0.0] — 2026-05-01
|
|
10
21
|
|
|
11
22
|
First public release. **19 MCP tools** for iOS leak hunting and performance investigation, plus a thin CLI mode for scripting and CI.
|
|
@@ -63,5 +74,6 @@ When called with no arguments it starts the MCP server over stdio.
|
|
|
63
74
|
- **`captureMemgraph`** does not work on physical iOS devices — `leaks(1)` only attaches to processes on the local Mac (which includes iOS simulators). Memory Graph capture from a physical device still requires Xcode.
|
|
64
75
|
- **`detectLeaksInXCUITest`** is flagged experimental: orchestration logic is implemented but not yet validated against a wide set of production XCUITest runs.
|
|
65
76
|
|
|
66
|
-
[Unreleased]: https://github.com/carloshpdoc/memorydetective/compare/v1.0.
|
|
77
|
+
[Unreleased]: https://github.com/carloshpdoc/memorydetective/compare/v1.0.1...HEAD
|
|
78
|
+
[1.0.1]: https://github.com/carloshpdoc/memorydetective/compare/v1.0.0...v1.0.1
|
|
67
79
|
[1.0.0]: https://github.com/carloshpdoc/memorydetective/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ memorydetective analyze ~/Desktop/myapp.memgraph
|
|
|
40
40
|
memorydetective classify ~/Desktop/myapp.memgraph
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
→ See [Examples](#examples) for chat-driven flows · [API](#api) for the full tool reference · [Configure](#configure) for Claude Desktop / Cursor / Cline.
|
|
43
|
+
→ See [Examples](#examples) for chat-driven flows · [API](#api) for the full tool reference · [Configure](#configure) for Claude Desktop / Cursor / Cline · [USAGE.md](./USAGE.md) for the full walkthrough including how fixes flow from diagnosis to your codebase.
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
package/USAGE.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Usage guide
|
|
2
|
+
|
|
3
|
+
Walkthrough of how `memorydetective` actually works in practice — what each tool returns, how fixes flow from diagnosis to your codebase, and the architecture decision behind splitting "diagnose" from "edit".
|
|
4
|
+
|
|
5
|
+
For a quick API reference, see the [`README.md`](./README.md). For the full changelog, see [`CHANGELOG.md`](./CHANGELOG.md).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Three ways to use it
|
|
10
|
+
|
|
11
|
+
### 1a. CLI mode — quickest way to see it work
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g memorydetective
|
|
15
|
+
memorydetective --version # 1.0.0
|
|
16
|
+
|
|
17
|
+
# Run analyze on any .memgraph file
|
|
18
|
+
memorydetective analyze ~/Desktop/myapp.memgraph
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
What you see (terminal output, ANSI-coloured):
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
┌─ memorydetective analyze ──────────────────────────────────────┐
|
|
25
|
+
│ Path: /Users/.../myapp.memgraph
|
|
26
|
+
│ Process: MyApp (pid 12345)
|
|
27
|
+
│ Bundle: com.example.myapp
|
|
28
|
+
└────────────────────────────────────────────────────────────────┘
|
|
29
|
+
|
|
30
|
+
60,436 leaks (7.89 MB)
|
|
31
|
+
4 ROOT CYCLE blocks
|
|
32
|
+
|
|
33
|
+
Top cycle: Swift._DictionaryStorage<SwiftUI.AnyHashable2, SwiftUI…
|
|
34
|
+
chain length: 545 nodes
|
|
35
|
+
app-level classes in chain: Closure context, DetailViewModel,
|
|
36
|
+
ItemRepositoryImpl, ItemGraphQLDataSource, GraphQLClient
|
|
37
|
+
|
|
38
|
+
Diagnosis:
|
|
39
|
+
60436 leaks; 4 ROOT CYCLE blocks. Largest top-level cycle:
|
|
40
|
+
Swift._DictionaryStorage… (chain of 545 nodes). App-level
|
|
41
|
+
classes in chains: Closure context, DetailViewModel, …
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then ask the classifier for fix advice:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
memorydetective classify ~/Desktop/myapp.memgraph
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
You see one block per ROOT CYCLE, like:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Root: Swift._DictionaryStorage<SwiftUI.AnyHashable2, SwiftUI…
|
|
54
|
+
Match: swiftui.tag-index-projection (high confidence)
|
|
55
|
+
Fix hint:
|
|
56
|
+
Replace `[weak self]` capture in tap closures with a static
|
|
57
|
+
helper, OR weak-capture the coordinator/view-model directly
|
|
58
|
+
with `[weak coord = self.coordinator]`. The `.tag()` modifier
|
|
59
|
+
on photo carousels is the usual culprit.
|
|
60
|
+
Also matched: swiftui.dictstorage-weakbox-cycle,
|
|
61
|
+
closure.viewmodel-wrapped-strong,
|
|
62
|
+
swiftui.foreach-state-tap
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 1b. JSON mode — for scripts and CI
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
memorydetective analyze ~/Desktop/myapp.memgraph --json | jq .totals
|
|
69
|
+
memorydetective classify ~/Desktop/myapp.memgraph --json | jq '.classified[0].primaryMatch'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The JSON shape mirrors the MCP tool's response — same fields, no ANSI colours, ready to pipe into anything.
|
|
73
|
+
|
|
74
|
+
### 1c. MCP mode — the actual product UX
|
|
75
|
+
|
|
76
|
+
This is what we built it for: an LLM agent (Claude Code, Claude Desktop, Cursor, Cline, Kiro, …) drives the investigation by chat.
|
|
77
|
+
|
|
78
|
+
Add to your MCP client config (Claude Code shown):
|
|
79
|
+
|
|
80
|
+
```jsonc
|
|
81
|
+
// ~/.claude/settings.json
|
|
82
|
+
{
|
|
83
|
+
"mcpServers": {
|
|
84
|
+
"memorydetective": { "command": "memorydetective" }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Open Claude Code in your iOS project and just ask:
|
|
90
|
+
|
|
91
|
+
> Diagnose `~/Desktop/myapp.memgraph` and find where to fix in this codebase.
|
|
92
|
+
|
|
93
|
+
Claude orchestrates the full flow (see [section 3](#3-how-fixes-actually-flow-from-diagnosis-to-edit)).
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 2. The 8 cycle patterns and their fix hints
|
|
98
|
+
|
|
99
|
+
`classifyCycle` ships with a built-in catalog of common iOS retain-cycle patterns. Each pattern returns a `fixHint` — a plain-English string describing the fix direction.
|
|
100
|
+
|
|
101
|
+
| Pattern ID | When it matches | Fix hint (summary) |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `swiftui.tag-index-projection` | `TagIndexProjection<Int>` appears in chain (`.tag()` modifier capturing self) | Replace `[weak self]` capture with a static helper, or weak-capture the coordinator/view-model directly. |
|
|
104
|
+
| `swiftui.dictstorage-weakbox-cycle` | Root is `_DictionaryStorage<…WeakBox<AnyLocationBase>>` | SwiftUI internal observation graph cycle. Find your app-level types in the chain and break the strong capture there. |
|
|
105
|
+
| `swiftui.foreach-state-tap` | `SwiftUI.ForEachState` in chain | ForEachState held by a tap-gesture closure capturing `self`. Make the tap handler a static function or capture properties weakly. |
|
|
106
|
+
| `closure.viewmodel-wrapped-strong` | `__strong` edge with `_viewModel.wrappedValue` in label | Closure captures `_viewModel.wrappedValue` strongly. Capture the underlying ObservableObject weakly: `[weak vm = _viewModel.wrappedValue]`. |
|
|
107
|
+
| `viewcontroller.uinavigationcontroller-host` | `UINavigationController` + `UIHostingController` both in chain | Clear `viewControllers = []` in `dismantleUIViewController` to break the host->VC->host cycle. |
|
|
108
|
+
| `combine.sink-store-self-capture` | `AnyCancellable` + `Closure context` | `.sink { self.x = … }` keeps self alive through the AnyCancellable that's stored on self. Capture explicitly: `.sink { [weak self] in self?.x = … }`. |
|
|
109
|
+
| `concurrency.task-without-weak-self` | `_Concurrency.Task<…>` + `Closure context` | `Task { }` body strongly captures self for the lifetime of the task. `Task { [weak self] in guard let self else { return }; … }`. |
|
|
110
|
+
| `notificationcenter.observer-strong` | `NotificationCenter` / `NSNotificationCenter` + `Closure context` | Block-form `addObserver(forName:...)` keeps the block alive in the center. Use `[weak self]` in the block, or store the returned `NSObjectProtocol` and call `removeObserver(_:)` in `deinit`. |
|
|
111
|
+
|
|
112
|
+
**Confidence tiers**: each pattern is checked at `high` first, then `medium`. If multiple patterns fire on the same cycle, all matches are returned — the highest-confidence one is `primaryMatch`, the rest are in `allMatches`.
|
|
113
|
+
|
|
114
|
+
**The hints are deliberately textual, not code patches.** That's by design — see the next section.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 3. How fixes actually flow from diagnosis to edit
|
|
119
|
+
|
|
120
|
+
`memorydetective` is the **diagnose** side. It tells you **what** is wrong, **where in the cycle**, and **what type of fix** is needed. It does **not** edit your code.
|
|
121
|
+
|
|
122
|
+
The **edit** side comes from your LLM agent (Claude Code, Cursor, Cline, …) using its native code-editing tools (Read, Grep, Edit, …).
|
|
123
|
+
|
|
124
|
+
This split is intentional:
|
|
125
|
+
|
|
126
|
+
- The fix depends on your real code (file paths, surrounding context, naming). `memorydetective` knows nothing about your codebase.
|
|
127
|
+
- LLM agents already excel at code editing. Letting them keep that role keeps `memorydetective` focused.
|
|
128
|
+
- The catalog of known patterns becomes the knowledge moat; the LLM brings the codebase awareness.
|
|
129
|
+
|
|
130
|
+
### Concrete end-to-end example
|
|
131
|
+
|
|
132
|
+
Configuration (one-time): `memorydetective` registered as an MCP server in Claude Code, as shown in [section 1c](#1c-mcp-mode--the-actual-product-ux).
|
|
133
|
+
|
|
134
|
+
You open Claude Code in your iOS project and ask:
|
|
135
|
+
|
|
136
|
+
> **You:** I exported `~/Desktop/myapp.memgraph` after closing some detail screens — something is leaking. Diagnose and find where to fix in this codebase.
|
|
137
|
+
|
|
138
|
+
What Claude does, step by step:
|
|
139
|
+
|
|
140
|
+
1. **Calls `analyzeMemgraph(path: "~/Desktop/myapp.memgraph")`**
|
|
141
|
+
- Receives: 60,436 leaks, 4 ROOT CYCLE blocks, top cycle is a `_DictionaryStorage` chain.
|
|
142
|
+
|
|
143
|
+
2. **Calls `classifyCycle(path: "~/Desktop/myapp.memgraph")`**
|
|
144
|
+
- Receives: pattern `swiftui.tag-index-projection` (high confidence) + fix hint about `.tag()` and weak capture.
|
|
145
|
+
|
|
146
|
+
3. **Uses its own code-editing tools to find the offending file:**
|
|
147
|
+
- `Grep` for `.tag(` in your `*.swift` files.
|
|
148
|
+
- `Grep` for `onImageSliderTap` (or whatever closure name surfaced in the chain).
|
|
149
|
+
- `Read` the candidate files to see how the closure is captured.
|
|
150
|
+
|
|
151
|
+
4. **Presents the proposed fix in chat:**
|
|
152
|
+
|
|
153
|
+
> **Claude:** Found the cycle. `WishlistPlaceDetailsView.swift:142` passes `onImageSliderTap` as a closure that captures `self`. The `.tag(index)` modifier on the carousel `ForEach` is what triggers the SwiftUI internal `TagIndexProjection` observation that pins the cycle.
|
|
154
|
+
>
|
|
155
|
+
> Suggested fix: hoist the handler to a `static` function and capture only what's needed weakly:
|
|
156
|
+
> ```swift
|
|
157
|
+
> // before
|
|
158
|
+
> onImageSliderTap: { index in
|
|
159
|
+
> self.viewModel.handlePhotoTap(at: index)
|
|
160
|
+
> }
|
|
161
|
+
>
|
|
162
|
+
> // after
|
|
163
|
+
> onImageSliderTap: { [weak vm = _viewModel.wrappedValue,
|
|
164
|
+
> weak coord = self.coordinator] index in
|
|
165
|
+
> Self.handlePhotoTap(index: index, viewModel: vm, coordinator: coord)
|
|
166
|
+
> }
|
|
167
|
+
>
|
|
168
|
+
> // plus a static helper:
|
|
169
|
+
> private static func handlePhotoTap(
|
|
170
|
+
> index: Int,
|
|
171
|
+
> viewModel: DetailViewModel?,
|
|
172
|
+
> coordinator: DetailsCoordinator?
|
|
173
|
+
> ) { /* ... */ }
|
|
174
|
+
> ```
|
|
175
|
+
>
|
|
176
|
+
> Want me to apply this?
|
|
177
|
+
|
|
178
|
+
5. **You accept** → Claude calls its own `Edit` tool → diff appears in your IDE → you review and commit.
|
|
179
|
+
|
|
180
|
+
6. **(Optional) Verify the fix:** export a fresh `.memgraph` after running the same flow, then ask Claude:
|
|
181
|
+
|
|
182
|
+
> **You:** Did the fix work? Compare `~/Desktop/myapp-before.memgraph` to `~/Desktop/myapp-after.memgraph`.
|
|
183
|
+
|
|
184
|
+
Claude calls `diffMemgraphs` — instance counts dropped, the `swiftui.tag-index-projection` cycle is gone from `cycles.persisted`, present in `cycles.goneFromBefore`.
|
|
185
|
+
|
|
186
|
+
### Why this is better than "memorydetective generates the diff"
|
|
187
|
+
|
|
188
|
+
If `memorydetective` tried to generate a code patch, it would have to:
|
|
189
|
+
- Parse Swift source
|
|
190
|
+
- Understand the file's import graph
|
|
191
|
+
- Track the actual variable names and types in scope
|
|
192
|
+
- Match surrounding code style
|
|
193
|
+
|
|
194
|
+
That's exactly what an LLM agent already does — and does well. Splitting the responsibility keeps each side simple. `memorydetective` knows **iOS perf**; the agent knows **your codebase**. They compose.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 4. Common follow-up requests
|
|
199
|
+
|
|
200
|
+
Once you have the diagnosis, here are useful follow-up prompts you can paste into Claude:
|
|
201
|
+
|
|
202
|
+
| Prompt | What Claude calls |
|
|
203
|
+
|---|---|
|
|
204
|
+
| "How many `DetailViewModel` instances are leaking?" | `countAlive(path, className: "DetailViewModel")` |
|
|
205
|
+
| "Show the retain chain that keeps `DetailViewModel` alive." | `findRetainers(path, className: "DetailViewModel")` |
|
|
206
|
+
| "Compare `~/Desktop/before.memgraph` to `~/Desktop/after.memgraph` — did the leak go away?" | `diffMemgraphs(before, after)` |
|
|
207
|
+
| "Render the cycle as a Mermaid graph for the PR description." | `renderCycleGraph(path, format: "mermaid")` |
|
|
208
|
+
| "Profile this app on my iPhone for 90 seconds and tell me about hangs." | `listTraceDevices` → `recordTimeProfile` → `analyzeHangs` |
|
|
209
|
+
| "Pull the last 5 minutes of `error`-level logs from `MyApp`." | `logShow(last: "5m", process: "MyApp", level: "default")` |
|
|
210
|
+
| "Run my XCUITest with leak detection." | `detectLeaksInXCUITest(workspace, scheme, testIdentifier, …)` |
|
|
211
|
+
|
|
212
|
+
The agent decides which tool to call based on your prompt — you don't need to remember the tool names.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 5. Troubleshooting
|
|
217
|
+
|
|
218
|
+
### `memorydetective: command not found`
|
|
219
|
+
|
|
220
|
+
The npm global install isn't on your `$PATH`. Check:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
which memorydetective
|
|
224
|
+
npm prefix -g
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
If `npm prefix -g` returns something not in your `$PATH`, add it. Or use the binary directly:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
$(npm prefix -g)/bin/memorydetective --version
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### `analyzeTimeProfile` returns a SIGSEGV notice
|
|
234
|
+
|
|
235
|
+
Known limit. `xcrun xctrace export` of the `time-profile` schema crashes on heavy unsymbolicated traces. Workarounds (in order of effort):
|
|
236
|
+
|
|
237
|
+
1. Open the trace once in Instruments.app (forces symbolication), then close it. Re-run `analyzeTimeProfile`.
|
|
238
|
+
2. Re-record with a shorter `--time-limit` (try 30 s instead of 90 s).
|
|
239
|
+
3. For hang analysis specifically, use `analyzeHangs` instead — it parses a different (lighter) schema that doesn't crash.
|
|
240
|
+
|
|
241
|
+
### `captureMemgraph` fails on a physical iOS device
|
|
242
|
+
|
|
243
|
+
By design. `leaks(1)` only attaches to processes on the local Mac (which includes iOS simulators). Memory Graph capture from a physical device goes through Xcode's debugger over USB — different mechanism, no public CLI equivalent. Use Xcode's Memory Graph button + File → Export Memory Graph for physical devices.
|
|
244
|
+
|
|
245
|
+
### Tests pass locally but fail in CI
|
|
246
|
+
|
|
247
|
+
The stress test has a wallclock budget that's tighter on slower runners. If you see `expected NNNms to be less than 2000`, bump `PARSE_BUDGET_MS` in `src/stress.test.ts`.
|
|
248
|
+
|
|
249
|
+
### `detectLeaksInXCUITest` says "after-capture failed"
|
|
250
|
+
|
|
251
|
+
The app process exited before `leaks --outputGraph` could attach. Configure your XCUITest to keep the app alive at end-of-test (e.g. `XCTAssertTrue(true); _ = XCTWaiter.wait(for: [...], timeout: 1.0)`), or use a longer simulator boot. This tool is **experimental** in v1.0 — feedback welcome.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 6. Where to go from here
|
|
256
|
+
|
|
257
|
+
- **Add a new cycle pattern**: see the *Adding a cycle pattern to `classifyCycle`* section in [`README.md`](./README.md#contributing).
|
|
258
|
+
- **Run a custom analysis from scratch**: every tool's input schema is documented via the MCP `tools/list` request. Hit the server with `{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}` over stdio.
|
|
259
|
+
- **Open an issue**: https://github.com/carloshpdoc/memorydetective/issues — bug reports, feature requests, and pattern contributions are all welcome.
|
package/dist/cli.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memorydetective",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "MCP server for iOS leak hunting and performance investigation. Reads .memgraph + .trace files, captures new ones via xctrace and leaks(1), classifies retain cycles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"dist",
|
|
12
12
|
"README.md",
|
|
13
13
|
"LICENSE",
|
|
14
|
-
"CHANGELOG.md"
|
|
14
|
+
"CHANGELOG.md",
|
|
15
|
+
"USAGE.md"
|
|
15
16
|
],
|
|
16
17
|
"scripts": {
|
|
17
18
|
"build": "tsc && chmod +x dist/index.js",
|