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 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.0...HEAD
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
@@ -24,7 +24,7 @@ const C = {
24
24
  cyan: "\x1b[36m",
25
25
  gray: "\x1b[90m",
26
26
  };
27
- const VERSION = "1.0.0";
27
+ const VERSION = "1.0.1";
28
28
  const HELP = `${C.bold}memorydetective${C.reset} — iOS leak hunting from the CLI
29
29
 
30
30
  ${C.dim}Usage:${C.reset}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorydetective",
3
- "version": "1.0.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",