# memorydetective
[English](README.md) · **Português brasileiro**
> Diagnostique retain cycles e regressões de performance iOS direto do seu chat. Sem precisar abrir o Xcode.
[](https://www.npmjs.com/package/memorydetective)
[](https://github.com/carloshpdoc/memorydetective/actions/workflows/ci.yml)
[](./LICENSE)
[](https://github.com/carloshpdoc/memorydetective/stargazers)
[](#requisitos)
[](#requisitos)

## Destaques
- **Caça de leaks via CLI.** Lê arquivos `.memgraph` capturados pelo Xcode (ou pelo próprio `memorydetective` em simuladores), encontra blocos ROOT CYCLE, classifica contra patterns conhecidos de SwiftUI/Combine, e devolve um fix hint em uma linha. Tudo via script ou chat.
- **MCP-nativo.** Plugga no Claude Code, Claude Desktop, Cursor, Cline, e qualquer outro cliente MCP. O agente conduz o loop completo investigar → classificar → sugerir fix sem você abrir o Instruments.
- **Honesto sobre os limites.** Sem outputs mockados, sem promessa exagerada. Análise de hangs funciona limpa via `xctrace`; Time Profile a nível de sample é parseado quando o `xctrace` simboliza, e retorna um workaround notice estruturado quando não consegue (o SIGSEGV do `xctrace` em traces pesados não simbolicados é uma limitação do lado da Apple que a gente expõe explicitamente). Captura de Memory Graph funciona em apps Mac e simulador iOS; iPhones físicos ainda precisam do Xcode.
> **Novidade na v1.18** (2026-05-17): MetricKit + audit-close. **`analyzeMetricKitPayload` é a 42ª ferramenta MCP**: ingere payloads JSON `.mxdiagnostic` do Apple MetricKit vindos de builds reais de TestFlight / App Store (diagnóstico post-mortem de produção, nenhum competidor MCP cobre essa lane hoje). Três outputs acionáveis: clusters de crash por exception type / binary / top frame, hotspots de hang com parsing de duração localizada (`"5.4 sec"` / `"20秒"`), exceções de CPU + disco. Cross-tool chain hints (ex: top frame estilo `objc_release` sugere `findCycles`). Mais três itens de audit-close: `SupportStatusKind` virou open-enum (consumidores downstream adicionam kinds sem breaking type bump), cache de `schemaDiscovery` invocation-scoped (`summarizeTrace` end-to-end ficou de ~28s para ~15s em traces reais da Apple, via um único TOC fetch up-front), e integration tests local-only contra `.trace` bundles reais da Apple (fecha de vez a classe de drift P+O da v1.14). 701 → 757 testes. 41 → 42 ferramentas MCP.
>
> **Também recente (v1.17)**: pass de confiabilidade. 14 bug fixes em três tiers. Headlines: parsing strtobool de envs truthy, modos de match no whitelist do `verifyFix` (exact / substring / regex), `recordViaInstrumentsApp` captura traces salvos fora do `watchDir`, fallback fault-tolerant no `inspectTrace`, filtro configurável de ruído de framework no `countAlive`, min/max/mediana de tamanho variável de classe.
>
> **E v1.16**: release de unblock de gravação no macOS 26.x. Nova ferramenta MCP `recordViaInstrumentsApp` envolve o fluxo GUI do Instruments.app: abre o app, expõe instruções passo a passo, monitora um diretório esperando o `.trace` salvo, e encadeia em `inspectTrace` no sucesso. Até a Apple corrigir a regressão do `xcrun xctrace record` nos simuladores macOS 26.x, esse é o caminho automatizado.
>
> **E v1.15**: cobertura de schema + UX de verify-fix. Três novas ferramentas MCP de trace fecharam o gap remanescente: `analyzeMemoryFootprint` (38ª, VM resident / dirty / virtual + diagnóstico de jetsam), `analyzeEnergyImpact` (39ª, investigação de drain de bateria), `analyzeLeakTimeline` (40ª, o instrument leaks do xctrace como série temporal). `summarizeTrace` agora encadeia `analyzeNetworkActivity`. `replayScenario` captura screenshots do simulador por step.
>
> **Anteriormente**: v1.14 confiabilidade do lado de trace, `analyzeNetworkActivity`, `supportStatus[]` unificado, view de tamanho `countAlive` inspirada no FLEX, whitelist `verifyFix` inspirada em MLeaksFinder + DebugSwift. v1.13 shipou `summarizeTrace` + prompt MCP `/summarize-trace`. v1.12 completou propagação reference-tree. v1.11 adicionou inspectTrace, reference-tree no diffMemgraphs. v1.9 shipou analyzeAbandonedMemory, detectLeaksInXCTest, cleanupTraces, mainThreadViolations. Notas completas no [CHANGELOG](./CHANGELOG.md).
> **Heads up para quem usa macOS 26.x:** a Apple shipou uma regressão de kernel no `task_for_pid` no macOS 26.x que bloqueia `leaks --outputGraph`, `heap`, E `xctrace --template Allocations` contra processos do simulador iOS independente de `MallocStackLogging`. Até o "View Memory Graph Hierarchy" do Xcode é atingido a menos que `Malloc Stack Logging` esteja ativo na aba Diagnostics do scheme. O memorydetective expõe isso como `platformAdvisory` proativo na primeira chamada de ferramenta classe-captura, mais um `workaroundNotice` com `issue: "macos-26-task-for-pid-broken"` se `leaks` for invocado. **O workaround mais confiável hoje é mirar um runtime de simulador iOS 18** (instala via Xcode > Settings > Platforms > +iOS 18.x). Validado empiricamente na [investigação notelet](https://github.com/carloshpdoc/memorydetective/blob/main/CHANGELOG.md#unreleased) em 2026-05-12, onde três caminhos independentes de introspecção de memória via CLI falharam até iOS 18 ser identificado como o escape hatch funcional. Defina `MEMORYDETECTIVE_SUPPRESS_PLATFORM_ADVISORY=1` para silenciar o aviso quando já tiver definido um workaround.
> **Também no macOS 26.x: `xctrace record` está quebrado para targets de simulador.** Independente da regressão `task_for_pid` acima, `xcrun xctrace record --time-limit Ns` contra processos do simulador iOS trava passando do time limit, eventualmente sai quando matado, e o `.trace` bundle resultante fica sem metadata de template. `xctrace export --toc` falha depois com `Document Missing Template Error`. Revalidado contra Xcode 26.5 (build 17F42, xctrace 16.0) em 2026-05-15: regressão sobrevive ao update. Isso atinge todo o ecossistema baseado em `xctrace` da mesma forma (`memorydetective.recordTimeProfile`, XcodeTraceMCP, e chamadas cruas de `xcrun xctrace record` falham idênticas). **Workarounds:** (1) **use `recordViaInstrumentsApp`** (v1.16, endurecido na v1.17), que abre o Instruments.app pra você, pede pra gravar + salvar o `.trace`, e encadeia em `inspectTrace` automaticamente quando o bundle aparece. v1.17 também captura saves fora do watch directory via query AppleScript no documento do Instruments.app, retornando `savedOutsideWatchDir: true` mais o path real; (2) gravar de um host macOS mais antigo com Xcode 26.0 se você tiver um; (3) gravar contra um device físico (a regressão parece ser simulator-specific). v1.17 adicionou uma viability probe no `recordTimeProfile` (`bundleStatus: "wedged"` quando o bundle no disco é o stub de 52K) e no `inspectTrace` (retorna `ok: true` com texto de diagnóstico ao invés de lançar). USAGE.md > Troubleshooting tem o passo a passo.
## Quickstart
```bash
npm install -g memorydetective
```
Adiciona no config do seu cliente MCP (Claude Code abaixo):
```jsonc
// ~/.claude/settings.json
{
"mcpServers": {
"memorydetective": { "command": "memorydetective" }
}
}
```
Ou usa direto pela linha de comando:
```bash
memorydetective analyze ~/Desktop/myapp.memgraph
memorydetective classify ~/Desktop/myapp.memgraph
```
→ Veja [Exemplos](#exemplos) para fluxos via chat · [API](#api) para a referência completa de ferramentas · [Configurar](#configurar) para Claude Desktop / Cursor / Cline · [USAGE.md](./USAGE.md) para o walkthrough completo incluindo como fixes fluem do diagnóstico pro seu codebase.
---
## Exemplos
> Trechos realistas de chat. O agente chama as ferramentas MCP listadas em [API](#api).
### Diagnosticar um leak a partir de um memgraph
> **Você:** Acabei de exportar um memory graph em `~/Desktop/example.memgraph`. Algo está vazando depois que fecho uma tela de detalhe. O que é?
>
> **Claude:** *(chama `analyzeMemgraph` → `classifyCycle`)*
> Achei 4 blocos ROOT CYCLE em 60.436 leaks. O dominante casa com **swiftui.tag-index-projection** com alta confiança. Um closure anexado via `.tag()` num `ForEach` de carousel de fotos está capturando `self` strong através de `_viewModel.wrappedValue`, o que mantém `DetailViewModel` e o coordinator de navegação vivos depois do dismiss.
>
> **Fix sugerido:** trocar o capture `[weak self]` no tap handler por um helper estático, ou capturar weak o view-model via `[weak coord = self.coordinator]`. Quer que eu encontre o arquivo exato?
### Verificar se um fix funcionou de verdade
> **Você:** Shipei o fix. Compara `~/Desktop/before.memgraph` com `~/Desktop/after.memgraph` e me diz se o count de `DetailViewModel` caiu.
>
> **Claude:** *(chama `diffMemgraphs` → `countAlive`)*
> `DetailViewModel`: **12 → 0** (delta `-12`). O ciclo `swiftui.tag-index-projection` sumiu. Contagem total de leaks caiu de 60.436 para 55.576 (−4.860). Três outros ROOT CYCLEs persistem. São internos do SwiftUI, não código seu.
### Loop self-contained de verify-fix (v1.8)
Quando `leaks --outputGraph` falha no macOS 26.x (`Failed to get DYLD info for task`), a causa geralmente é que o app não foi lançado com `MallocStackLogging=1`. As novas ferramentas de orquestração lidam com isso end to end sem você ter que conectar xcodebuild + simctl + axe na mão.
> **Você:** Reproduz o leak no `MyApp` pra mim. Buila, lança com as env vars certas, repete o fluxo do carousel de fotos 5 vezes pra amplificar, captura before, daí eu shipo o fix e você captura after.
>
> **Claude:** *(chama `bootAndLaunchForLeakInvestigation`)*
> Buildei + bootei iPhone 15 (UDID `ABCD-...`). Lancei `MyApp` (PID `49581`) com `MallocStackLogging=1`. Bundle id `com.example.MyApp`.
>
> *(chama `replayScenario` com 5 taps no carousel + ciclo de back, repete 5)*
> Executei 25 ações em 12.3s. Sem falhas.
>
> *(chama `captureScenarioState({label:"before"})`)*
> Escreveu `before.memgraph` (63.201 leaks), `before.png`, `before.ui.json`. `MallocStackLogging` ativo, sem workaroundNotice.
>
> *(você shipa o fix e rebuilda)*
>
> *(chama `bootAndLaunchForLeakInvestigation` + replay + `captureScenarioState({label:"after"})`)*
>
> *(chama `diffMemgraphs` → `verifyFix`)*
> `DetailViewModel`: **15 → 0**. `swiftui.tag-index-projection` PASS. Bytes liberados: 28.4 MB.
### Achar o caminho lento num device real
> **Você:** Faz profile do DemoApp no meu iPhone por 90 segundos e me diz onde estão os hangs.
>
> **Claude:** *(chama `listTraceDevices` → `recordTimeProfile` → `analyzeHangs`)*
> Gravei `~/Desktop/run.trace` (90s, attached no DemoApp num iPhone 17 Pro Max). Encontrei **23 hangs visíveis ao usuário** (>500 ms cada), o mais longo 1.16 s, média 769 ms. Carga severa. Todos na main thread. Hotspots a nível de sample não estão simbolizados ainda; abre o trace no Instruments pra esse step (`analyzeTimeProfile` retorna um workaround notice estruturado quando o `xctrace` não consegue simbolizar. Veja limits conhecidos no CHANGELOG).
### Analisar payloads MetricKit de crashes reais (v1.18)
> **Você:** Tenho uma pasta de `.mxdiagnostic` dos últimos 30 dias de TestFlight em `~/Downloads/metrickit-may/`. O que está pegando usuários reais?
>
> **Claude:** *(chama `analyzeMetricKitPayload({ payloadDir: "~/Downloads/metrickit-may/" })`)*
> 47 payloads, 2026-04-15 → 2026-05-15. Top: **18 crashes EXC_BAD_ACCESS** clusterizados no mesmo top frame, todos nos builds 456 e 457 (limpo no 458 + anteriores). Pattern casa com release de retain-cycle; chain hint sugere `findCycles` no próximo passo.
>
> Segundo: **hang de 5.4s** em `libsqlite3.dylib` (3 samples). Pattern `db-lock`. Se você reproduzir localmente, encadeia em `analyzeHangs` com `includeStackClassification: true` pro classifier `mainThreadViolations[]`.
>
> Sem frames simbolizados na v1.18 (shipa raw `binaryUUID + offset`; dSYM lookup é tool separado, release futura). Strings de hang localizadas tratadas: input foi `"5.4 sec"`, normalizado pra 5400ms; um diagnóstico veio com `"20秒"` (japonês), normalizado pra 20000ms.
Veja [USAGE.md § MetricKit production post-mortem](./USAGE.md#end-to-end-example-metrickit-production-post-mortem-v118) pro end-to-end completo incluindo como ligar `analyzeMetricKitPayload` num loop verify-fix entre múltiplos builds do TestFlight.
### End-to-end: leak → arquivo → sugestão de fix
O loop completo, do artefato até a edição de código. Combina análise de memory graph com source-bridging de Swift.
> **Você:** Tem um leak. Memgraph em `~/Desktop/example-leaks.memgraph`. O ciclo aponta pra `DetailViewModel`. Acha onde ele vive no projeto `~/Development/myapp/` e sugere um fix.
>
> **Claude:** *(chama `analyzeMemgraph` → `classifyCycle` → `reachableFromCycle`)*
> Achei **4 blocos ROOT CYCLE**. O dominante casa com `swiftui.tag-index-projection` com alta confiança. `reachableFromCycle` confirma 4 instâncias de `DetailViewModel` segurando ~1100 `NSURLSessionConfiguration` cada. A VM é a culpada, o stack de rede é dano colateral.
>
> *(chama `swiftSearchPattern` para localizar patterns `.tag(` + `onImageSliderTap` em `~/Development/myapp/Sources/`)*
> 1 match em `MyApp/Views/MediaCarousel.swift:142`.
>
> *(chama `swiftGetSymbolDefinition({ symbolName: "DetailViewModel", projectRoot: "...", candidatePaths: ["MyApp/ViewModels/"] })`)*
> Declarado em `MyApp/ViewModels/DetailViewModel.swift:18`. `class DetailViewModel: ObservableObject`.
>
> *(chama `swiftFindSymbolReferences` pra escopar o raio de explosão. 7 callsites)*
>
> **Fix sugerido em `MediaCarousel.swift:142`:**
> ```swift
> // antes
> .tag(index)
> .onImageSliderTap { index in
> self.viewModel.handlePhotoTap(at: index)
> }
>
> // depois. Helper estático + captures weak
> .tag(index)
> .onImageSliderTap { [weak vm = _viewModel.wrappedValue,
> weak coord = self.coordinator] index in
> Self.handlePhotoTap(index: index, viewModel: vm, coordinator: coord)
> }
> ```
> Quer que eu aplique e verifique com um `.memgraph` fresco?
---
## O que isso economiza
A pitch em uma frase: **`memorydetective` transforma um memgraph binário de 50-500 MB (ou um dump texto de `leaks(1)` de 200 KB) num resumo estruturado de 2-5 KB que seu agente de AI consegue raciocinar de verdade.** Isso muda a economia de usar um LLM pra investigação de perf iOS.
### Tokens (quando emparelhado com agente de AI tipo Claude / Cursor / Cline)
Uma investigação real de retain-cycle, rodada duas vezes. Uma com `memorydetective`, outra com o agente lendo o output cru do `leaks(1)` direto:
| Step | Sem MCP (agente lê output cru) | Com `memorydetective` |
|---|---|---|
| Carregar dump texto do `leaks` (~280 KB) | ~70.000 input tokens | n/a |
| Resumo do `analyzeMemgraph` | n/a | ~750 input tokens |
| `classifyCycle` + fix hint | agente re-raciocina sobre o dump por follow-up (3-4 turns extras) | 1 turn, `patternId` + `fixHint` estruturados |
| `findRetainers` / `reachableFromCycle` | agente re-escaneia o dump | ~500 tokens, query escopada |
| **Líquido por investigação** | ~85.000 tokens, ~6 turns | ~3.000 tokens, ~2 turns |
**Traduz pra aproximadamente $0.40-$1.20 por investigação** dependendo do modelo (Claude Opus / Sonnet / Haiku). Compõe linearmente com tamanho de arquivo e profundidade de investigação.
### Tempo de desenvolvedor
A mesma investigação, medida pelo dev:
| Step | Sem MCP | Com `memorydetective` |
|---|---|---|
| Capturar memgraph + rodar `leaks` | 5 min | 5 min (igual) |
| Ler & interpretar dump texto do `leaks` | 15-30 min (vasculhar 200 KB de frames repetitivos) | 30 seg (ler resumo de 3 KB) |
| Identificar o pattern responsável | 10-20 min (reconhecer a forma do ciclo pela experiência) | instantâneo (classifier retorna `patternId` + fix hint) |
| Localizar o tipo suspeito no source | 10-15 min (grep + navegação manual) | 30 seg (`swiftGetSymbolDefinition` retorna `file:line`) |
| Achar todo callsite pra medir raio de explosão do fix | 5-10 min (Xcode / grep) | 10 seg (`swiftFindSymbolReferences`) |
| **Wall-clock líquido** | **45-80 min** | **~10 min** |
Números arredondados de uma única investigação real anonimizada (um retain cycle SwiftUI sobre um `ForEach` tageado que segurava ~28 MB de state da network stack). Seu mileage varia com complexidade do ciclo e tamanho do codebase.
### Quando o ganho é marginal
Sendo honesto sobre onde isso **não** ajuda muito:
- **Memgraphs minúsculos** (ciclo único, < 50 KB raw): overhead do MCP é roughly token-neutro vs. leitura crua. O ganho de tempo dev ainda vale (sem parsing manual de ciclo) mas o ganho de token encolhe.
- **Lookups de símbolo one-shot** sem leak associado: usa `grep`, não precisa disso.
- **Primeira investigação num codebase novo**: o agente ainda precisa de turns de orientação independente de MCP. Os ganhos compostos chutam na *segunda* investigação em diante, quando o agente já cacheou a forma do projeto.
O ganho compõe com **(a)** tamanho de arquivo, **(b)** profundidade de investigação (multi-turn), e **(c)** quantos leaks você investiga por trimestre. Pra um dev solo corrigindo um leak por ano, o valor é principalmente o ganho de tempo. Pra um time rodando CI gates com `verifyFix` em todo PR, os ganhos de token + tempo somam em centenas de runs.
---
## Configurar
O binário `memorydetective` fala MCP por stdio. Aponta qualquer cliente compatível com MCP pra ele.
Claude Code
```jsonc
// ~/.claude/settings.json (global) ou .mcp.json (por projeto)
{
"mcpServers": {
"memorydetective": { "command": "memorydetective" }
}
}
```
Claude Desktop
```jsonc
// ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"memorydetective": { "command": "memorydetective" }
}
}
```
Restart o Claude Desktop depois de editar.
Cursor
```jsonc
// ~/.cursor/mcp.json
{
"mcpServers": {
"memorydetective": { "command": "memorydetective" }
}
}
```
Cline (VS Code)
```jsonc
// VS Code settings.json
{
"cline.mcpServers": {
"memorydetective": { "command": "memorydetective" }
}
}
```
Kiro
Kiro suporta servers MCP via config global. O bloco espelha o do Claude Desktop:
```jsonc
{
"mcpServers": {
"memorydetective": { "command": "memorydetective" }
}
}
```
Consulte os docs de setup MCP do Kiro pro caminho exato do arquivo de config no seu sistema.
GitHub Copilot (experimental)
GitHub Copilot suporta servers MCP em modo Agent (VS Code 1.94+). Adiciona em `.vscode/mcp.json` no seu repo:
```jsonc
{
"servers": {
"memorydetective": {
"type": "stdio",
"command": "memorydetective"
}
}
}
```
A integração MCP do Copilot anda rápido. Se esse snippet estiver stale, veja os [docs MCP do VS Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers).
### Variáveis de ambiente
Toda flag booleana `MEMORYDETECTIVE_*` abaixo aceita o set **strtobool** truthy (case-insensitive): `1 / true / t / yes / y / on` (truthy) e `0 / false / f / no / n / off` (falsy). Valores não reconhecidos emitem um warning one-time no stderr por variável e caem no default documentado. Pré-v1.17 o parser era `1`-only, o que causava no-ops silenciosos quando operadores exportavam `=true` ou `=yes`. O warning de advisory é gated pelo `MEMORYDETECTIVE_SUPPRESS_PLATFORM_ADVISORY`.
| Variável | Default | Efeito |
|---|---|---|
| `MEMORYDETECTIVE_REDACTION` | `balanced` | Scrubbing de output aplicado em toda resposta de ferramenta. `balanced` colapsa paths de home directory pra `~/...` e mascara secrets em forma de token (AWS keys, GitHub PATs, Stripe, Slack, Bearer auth). `strict` adiciona masking de hostname, IPv4 e bundle-identifier. `off` desabilita redação (útil pra debug local-only). Modo é logado uma vez no startup do server. |
| `MEMORYDETECTIVE_ALLOW_LAUNCH` | unset | Booleano (strtobool). Permite `bootAndLaunchForLeakInvestigation`. A ferramenta executa `xcodebuild` e `xcrun simctl launch` contra paths e bundle ids fornecidos pelo caller, então opt-in é necessário. Sem o gate, a ferramenta retorna `ok: false` com `state: launchNotAllowed` e uma explicação clara. Define isso só quando você confia nos inputs que o agente está produzindo. |
| `MEMORYDETECTIVE_MAX_RECORDING_SECONDS` | `300` | Cap no `recordTimeProfile.durationSec`. Requests acima do cap são rejeitados com erro claro. Bounded internamente num ceiling hard de 3600s (1h) pra que uma env var malconfigurada não desabilite o gate. |
| `MEMORYDETECTIVE_TRACE_ROOT` | `~/Library/Application Support/memorydetective/traces` | Diretório usado quando `recordTimeProfile.output` é um path relativo. Paths absolutos bypassam esse default para backwards-compat v1.8. Também o path default de scan pro `cleanupTraces`. O diretório é auto-criado no primeiro write. |
| `MEMORYDETECTIVE_ALLOW_EXTERNAL_CLEANUP` | unset | Booleano (strtobool). Permite `cleanupTraces` escanear e deletar `.trace` bundles FORA do `MEMORYDETECTIVE_TRACE_ROOT`. Sem ele, requests que resolvem fora do root configurado retornam `ok: false` com a razão da falha e não deletam nada. Default-deny em operações destrutivas de disco fora do boundary configurado. |
| `MEMORYDETECTIVE_SUPPRESS_PLATFORM_ADVISORY` | unset | Booleano (strtobool). Silencia o advisory de plataforma macOS 26.x que captureMemgraph, captureScenarioState e bootAndLaunchForLeakInvestigation emitem no primeiro uso. Também silencia os warnings v1.17 de stderr emitidos em valores booleanos não reconhecidos (qualquer flag `MEMORYDETECTIVE_*`) e em falhas de TOC fetch do `schemaDiscovery`. Útil quando você já tem um runtime de sim iOS 18 instalado e não precisa dos lembretes. |
| `MEMORYDETECTIVE_AUTO_OPEN_INSTRUMENTS` | unset | Booleano (strtobool). Faz `recordTimeProfile` invocar `open -a Instruments ` como escape hatch fire-and-forget quando o xctrace dá timeout (a regressão do macOS 26.x). v1.17 adiciona uma check de viabilidade no `MANIFEST.plist` antes de abrir, pra que o caminho auto-open pule bundles wedged de 52K stub (que de outra forma apresentariam um diálogo "Document Missing Template Error" no Instruments.app). O campo `openedInInstrumentsApp` da resposta reporta se o open foi invocado; `bundleStatus` (v1.17) reporta se o bundle no disco é `unknown` / `salvageable` / `wedged`. |
| `MEMORYDETECTIVE_PREFLIGHT_XCTRACE` | unset (auto) | Booleano (strtobool) + `auto`. Controla a probe pre-flight no `recordTimeProfile` que detecta o wedge xctrace do macOS 26.x em ~3-5 segundos ao invés de pagar o `durationSec` completo do usuário mais 30s de grace. Truthy força on independente de plataforma / target. Falsy força off. Quando unset, a probe auto-enable em sim macOS 26.x com attach (a combo known-broken) e fica off no resto. Pre-flight é skipado para modo `--launch` pra evitar double-launch do app. Side-effect do auto-enable: probe de 2 segundos roda antes da gravação completa. |
---
## API
**42 ferramentas MCP + 34 Resources + 7 Prompts**, agrupadas por propósito. Descrições de ferramentas são tagueadas com prefixo de categoria (`[mg.memory]`, `[mg.trace]`, `[mg.build]`, `[mg.scenario]`, `[mg.code]`, `[mg.log]`, `[mg.render]`, `[mg.ci]`, `[mg.discover]`, `[ops]`, `[meta]`) pra que ferramentas relacionadas fiquem visíveis a olho.
Muitas ferramentas incluem um campo `suggestedNextCalls` na resposta. Lista tipada de entradas `{ tool, args, why }` pré-populadas pelo resultado atual, pra que o LLM orquestrador encadeie calls sem re-raciocinar. Comece com `getInvestigationPlaybook(kind)` pra sequência canônica. Ou só digita `/investigate-leak` (um dos [Prompts](#prompts-7)) em qualquer cliente que expõe slash commands MCP.
O classifier de ciclos shipa **36 antipatterns nomeados** cobrindo SwiftUI (incluindo a era Swift 6 / `@Observable` / SwiftData / NavigationStack, mais a forma v1.9 `swiftui.observable-write-on-every-render`), Combine, Swift Concurrency (incl. AsyncSequence-on-self e a nova API `Observations`), UIKit (Timer/CADisplayLink/UIGestureRecognizer/KVO/URLSession/WebKit/DispatchSource, mais a forma v1.9 `uikit.viewcontroller-retained-after-pop`), Core Animation, Core Data, pattern Coordinator, e as libs third-party populares RxSwift + Realm. Cada pattern carrega:
- um `fixHint` textual de uma linha
- um tier de confiança (`high` / `medium` / `low`)
- um `staticAnalysisHint` apontando pra regra SwiftLint que complementa a evidência runtime (ou um aviso explícito de gap quando não existe regra. Reforça o diferencial: memorydetective enxerga o que linters perdem em parse time)
- um `fixTemplate` com snippets Swift concretos before/after (novo em v1.7) que o agente pode adaptar direto pro código do user via as ferramentas SourceKit-LSP de source-bridging
### Ler & analisar (14)
> Todos os 9 analisadores de trace abaixo aceitam um segundo argumento opcional `AnalyzeTraceOptions` (v1.18 D-02). Quando chamados pelo `summarizeTrace` (que roda schema discovery uma vez up front), o cache é forwarded pra que as calls `xctrace --toc` por analisador sejam skipadas. Callers diretos deixam a opção unset e o comportamento é idêntico ao v1.17.
| Ferramenta | O que faz |
|---|---|
| `analyzeMemgraph` | Roda `leaks` contra um `.memgraph` e retorna resumo (totais, blocos ROOT CYCLE, diagnóstico em inglês plain). |
| `findCycles` | Extrai só os blocos ROOT CYCLE como chains flattened, com filtro opcional de substring `className`. |
| `findRetainers` | "Quem está mantendo `` vivo?". Retorna paths de retain chain de um nó top-level até o match. |
| `countAlive` | Conta instâncias por classe. Forneça `className` pra um número, ou omita pra top-N de classes mais leaked. v1.17: filtro de ruído configurável (`excludeFrameworkNoise`, `additionalNoisePatterns`, `unsuppressClassPatterns`, `noiseAuditMode`) pra que a view acionável seja tunável por app. Classes de tamanho variável reportam `instanceSizeBytesMin / Max / Median` (era valor first-observed pré-v1.17). |
| `reachableFromCycle` | Reachability escopada por ciclo. "Quantas instâncias de `` são reachable do ciclo rooted em ``?". Distingue o culpado real das suas dependências retidas. |
| `diffMemgraphs` | Compara dois snapshots `.memgraph`: deltas totais + mudanças de class-count + ciclos new/gone/persisted. |
| `analyzeAbandonedMemory` | Diff de dois snapshots `.memgraph` nos counts de classe da reference-tree de heap (não na lista de ciclos) e classifica cada classe que cresceu como `kvo-observer-orphaned`, `notificationcenter-observer-leaked`, `cache-too-aggressive`, `singleton-retains-payload`, ou `unknown-growth`. Expõe a família de bugs que `leaks(1)` reporta como `leakCount: 0` porque não existe ciclo estrito. v1.10 adiciona `actionableGrowth[]` + `actionableShrinkage[]` (views filtradas de ruído de framework) e suporta `outputFormat: "verify-fix-table"` que emite uma tabela markdown focada Class \| Before \| After \| Delta diretamente. |
| `verifyFix` | Diff cycle-semântico: veredicto PASS/PARTIAL/FAIL por pattern + bytes liberados. CI-gateable. Whitelist `expectedAliveClasses` (v1.14) reserva singletons / caches / windows retidas pelo OS pra não votarem FAIL; v1.17 estende cada entry pra matching per-mode (`{ pattern, mode: "exact" \| "substring" \| "regex" }`), com strings bare mantendo o default substring. |
| `classifyCycle` | Casa cada ROOT CYCLE contra um catálogo built-in de **36 antipatterns nomeados** (SwiftUI / Combine / Concurrency / UIKit / Core Animation / Core Data / Coordinator / RxSwift / Realm) com confiança + `fixHint` textual + `staticAnalysisHint` (qual regra SwiftLint complementa isso, ou gap explícito) + `fixTemplate` (snippet Swift before/after). |
| `analyzeHangs` | Parseia o schema `potential-hangs` do `xctrace`; retorna counts Hang vs Microhang + top N mais longos. Passe `topFramesByHangStartNs` (tipicamente de um `analyzeTimeProfile` encadeado) pra enriquecer cada top hang com `mainThreadViolations[]` classificando o bloqueador como `sync-io`, `db-lock`, `network`, ou `lock-contention`. |
| `analyzeAnimationHitches` | Parseia o schema `animation-hitches` do `xctrace`; reporta counts por tipo e quantos hitches cruzaram o threshold de 100ms perceptível ao usuário da Apple. |
| `analyzeTimeProfile` | Parseia o schema `time-profile` do `xctrace`; retorna top symbols por sample count. Reporta SIGSEGV com workarounds quando xctrace não consegue simbolizar. |
| `analyzeAllocations` | Parseia o schema `allocations` do `xctrace`; retorna agregados por categoria (bytes cumulativos, count de alocação, lifecycle = transient/persistent/mixed) e top alocadores. |
| `analyzeAppLaunch` | Parseia o schema `app-launch` do `xctrace`; retorna tipo de launch cold/warm + breakdown por fase (process-creation, dyld-init, ObjC-init, AppDelegate, first-frame). |
| `logShow` | Query one-shot do unified logging do macOS via `log show --style compact` com filtros de predicate / process / subsystem. Retorna entries parseadas (timestamp, type, process, subsystem, category, message). |
### Capturar / gravar (4)
| Ferramenta | O que faz | Sim | Device |
|---|---|---|---|
| `recordTimeProfile` | Envolve `xcrun xctrace record --template "Time Profiler" --attach ... --time-limit Ns --output ...`. Retorna `bundleStatus: "unknown" \| "salvageable" \| "wedged"` (v1.17) pra que callers branchem na realidade on-disk após timeout ao invés de confiar no `tracePath` cegamente. O caminho auto-open (`MEMORYDETECTIVE_AUTO_OPEN_INSTRUMENTS`) probes `MANIFEST.plist` antes de lançar Instruments.app pra skipar stubs wedged de 52K. | ✅ | ✅ |
| `recordViaInstrumentsApp` | Escape hatch macOS 26.x (v1.16). Abre Instruments.app via `open -a Instruments`, retorna um array `instructions[]` dizendo ao user qual template escolher + quando apertar Record / Stop / Save, e depois polla `watchDir` a cada 5s por novos `.trace` bundles (mtime-stable por 10s). v1.17: também consulta Instruments.app rodando via AppleScript a cada poll por qualquer documento salvo fora do `watchDir`. Em match, retorna o path com `savedOutsideWatchDir: true` pra que users que apertaram Save e aceitaram o default Desktop não tomem timeout mais. Encadeia em `inspectTrace` no sucesso. | ✅ | ✅ |
| `captureMemgraph` | Envolve `leaks --outputGraph `. Resolve `appName → pid` via `pgrep -x`. Retorna um `workaroundNotice` estruturado na regressão macOS 26.x `Failed to get DYLD info for task` com issue ids estáveis (`minimal-corpse`, `permission-denied`, `leaks-not-found`, `transient`) e um fallback path pro `recordTimeProfile` (Allocations) + `analyzeAllocations`. | ✅ | ❌. Use Xcode |
| `logStream` | Envolve `log stream --style compact` por uma duração bounded (≤ 60 s). Retorna entries parseadas coletadas durante a janela. | n/a | n/a |
### Orquestração verify-fix (3, v1.8)
Essas três ferramentas combinam num único loop determinístico verify-fix: lança o app com `MallocStackLogging=1` pra que leaks funcione, dirige a UI pra amplificar o leak suspeito, snapshot before, shipa o fix, snapshot after, depois `diffMemgraphs`.
| Ferramenta | O que faz |
|---|---|
| `bootAndLaunchForLeakInvestigation` | Single-call build + boot + install + launch com `MallocStackLogging=1` propagado via `SIMCTL_CHILD_*`. Resolve o simulador (udid, name+os, ou o que estiver booted), descobre `BUILT_PRODUCTS_DIR` / `WRAPPER_NAME` / `EXECUTABLE_NAME` / `PRODUCT_BUNDLE_IDENTIFIER` do `xcodebuild -showBuildSettings -json`, e retorna o PID do host + UDID + bundle id pronto pra encadear em `captureMemgraph`. Necessário porque `leaks --outputGraph` regrediu no macOS 26.x e só funciona quando o target foi lançado com malloc-stack-logging no ambiente. |
| `replayScenario` | Conduz o iOS Simulator por ações tap / swipe / wait / type com count `repeat` pra amplificar leaks que só manifestam após N iterações. Targets de tap aceitam `label`, `elementId`, ou `coords`. Soft dependency no CLI [axe](https://github.com/cameroncooke/AXe) do Cameron Cooke. |
| `captureScenarioState` | Snapshot composto pra verify-fix: escreve `.memgraph` + screenshot `.png` + accessibility tree `.ui.json` em `outputDir`, todos prefixados pelo `label` (tipicamente `before` / `after`). Sub-capturas são best-effort: se leaks falhar no macOS 26.x, screenshot + UI tree ainda completam e o workaroundNotice do `captureMemgraph` é expressado via `memgraphWorkaroundNotice`. |
### Descobrir (3)
| Ferramenta | O que faz |
|---|---|
| `listTraceDevices` | Parseia `xcrun xctrace list devices` (devices + simulators + UDIDs). |
| `listTraceTemplates` | Parseia `xcrun xctrace list templates` (standard + custom). |
| `inspectTrace` | Ferramenta de orientação para `.trace` bundles. Retorna schemas presentes + counts de linha + metadata de device/OS/template + `suggestedNextCalls[]` mapeando cada schema known populado pro seu analisador. Use isso como a PRIMEIRA call em qualquer `.trace`. Novo em v1.11. v1.17: fault-tolerant, retorna `ok: true` com `schemas: []` e uma string de diagnóstico quando `xctrace export --toc` falha em bundles wedged de 52K, ao invés de throw. |
### Sintetizar (1)
| Ferramenta | O que faz |
|---|---|
| `summarizeTrace` | Call único que encadeia `inspectTrace` + os 5 analisadores em paralelo + cross-correlate findings (hangs sobrepondo com hitches, etc.) + pré-renderiza um cartão markdown compacto (<10 KB) com headline de 1 sentença, sub-sections por área, e suggestedNextCalls. A jogada "trace-pra-summary-card-em-uma-call". Use isso quando quer um pass de síntese ao invés de encadear 5-6 analisadores na mão. Novo em v1.13. v1.18 D-02: roda schema discovery uma vez up front e compartilha o cache com todos os 6 analisadores, economizando 600-3000ms de wall-clock em traces reais da Apple. |
### Diagnóstico de produção (1, v1.18)
| Ferramenta | O que faz |
|---|---|
| `analyzeMetricKitPayload` | Ingere payloads JSON Apple MetricKit `.mxdiagnostic` vindos de builds reais de TestFlight / App Store (nenhum competidor MCP cobre essa lane hoje). Três formas de input: `payloadPath` (arquivo único), `payloadDir` (agrega todos os `.mxdiagnostic` num diretório), `payloadJson` (raw, in-memory). Três seções de output: `crashCluster[]` (agrupado por `exception-type` / `binary` / `top-frame`, cada entry carrega `topFrame` + `affectedBuilds[]` + raw `binaryUUID + offset` pra simbolização dSYM downstream), `hangHotspots[]` (sorted por `hangDurationMs` com parsing de duração localizada: `"5.4 sec"` / `"20秒"` / etc.), `cpuExceptions[]` + `diskWriteExceptions[]`. Emite 4 novos valores `SupportStatusKind`. Cross-tool chain hints disparam automático (top frame estilo `objc_release` → `findCycles`; top frame `libsqlite3` → `analyzeHangs` com classifier main-thread-violation). SEM simbolização em v1; bytes raw apenas. Simulador NÃO gera payloads MetricKit (limitação do lado Apple); posicionado como analisador **post-mortem**, não captura live. Novo em v1.18. |
### Renderizar (1)
| Ferramenta | O que faz |
|---|---|
| `renderCycleGraph` | Lê um `.memgraph`, escolhe um ROOT CYCLE, e emite um grafo Mermaid (markdown-embeddable) ou Graphviz DOT. Classes a nível de app destacadas em vermelho; terminadores CYCLE BACK em âmbar. |
### Ops (1)
| Ferramenta | O que faz |
|---|---|
| `cleanupTraces` | Preview e delete de `.trace` bundles sob `MEMORYDETECTIVE_TRACE_ROOT`. `dryRun: true` por default (o agente tem que opt-in pra deleção). Para no boundary do `.trace` (NÃO desce DENTRO de bundles). Roots externos exigem `MEMORYDETECTIVE_ALLOW_EXTERNAL_CLEANUP=1` (default-deny). Útil como call periódica depois que algumas sessions de `recordTimeProfile` acumularam dezenas a centenas de MB de traces. |
### Integração CI / test (3)
| Ferramenta | O que faz |
|---|---|
| `detectLeaksInXCTest` | Buila o scheme de unit-test, roda com filtro opcional `-only-testing:`, captura baseline `.memgraph` + after contra o runner `xctest` (ou um `processName` custom pra bundles app-hosted), diff. Retorna `passed: false` quando novos ROOT CYCLEs aparecem que não estão na allowlist do user. Define `outputHtmlPath` pra também escrever um relatório HTML self-contained. CI-runnable. |
| `detectLeaksInXCUITest` | Irmão XCUITest: buila o workspace, roda o XCUITest nomeado, captura baseline `.memgraph` + after contra o app host, diff. Retorna `passed: false` quando novos ROOT CYCLEs aparecem que não estão na allowlist do user. Define `outputHtmlPath` pra também escrever um relatório HTML self-contained. CI-runnable. |
| `compareTracesByPattern` | Contraparte trace-side do `verifyFix`. Compara dois `.trace` bundles por categoria de perf (`hangs`, `animation-hitches`, ou `app-launch`) e retorna PASS/PARTIAL/FAIL com stats before/after e deltas. Aplica thresholds: hangs PASS quando o mais longo está abaixo de `hangsMaxLongestMs`; hitches PASS quando o mais longo está abaixo de `hitchesMaxLongestMs` (default 100ms. Threshold perceptível ao usuário da Apple); app-launch PASS quando total está abaixo de `appLaunchMaxTotalMs` (default 1000ms). |
#### Adicione memorydetective no seu CI em 5 minutos
`detectLeaksInXCTest` + `outputHtmlPath` são os building blocks pra um gate de leak per-PR. O job abaixo roda o scheme de unit-test nomeado em todo push e PR, faz upload do relatório HTML como workflow artifact, e falha quando novos ROOT CYCLEs aparecem fora da allowlist. Copia o arquivo pra `.github/workflows/leaks.yml` e ajusta o workspace + scheme + test identifier:
```yaml
name: leaks
on: [push, pull_request]
jobs:
detect-leaks:
runs-on: macos-14
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- run: sudo xcode-select -s /Applications/Xcode_15.4.app
- run: npm install -g memorydetective
- run: |
xcrun simctl boot "iPhone 15" || true
xcrun simctl bootstatus "iPhone 15" -b
- run: |
cat > leaks.json < **Por que `captureMemgraph` não funciona em devices iOS físicos**: `leaks(1)` só attacha a processos rodando no Mac local (que inclui simuladores iOS). Captura de Memory Graph de um device real passa pelo debugger do Xcode via USB/lockdownd. Mecanismo diferente, sem CLI equivalente público.
### Resources (34)
O catálogo de patterns de ciclo também é expressado como resources MCP, browsáveis em `memorydetective://patterns/{patternId}`. Cada resource é um body markdown com o nome do pattern, uma descrição mais longa, e o fix hint. Use isso pra deixar um agente (ou um humano num cliente MCP UI-aware) browse o catálogo sem queimar uma call `classifyCycle`.
```
memorydetective://patterns/swiftui.tag-index-projection
memorydetective://patterns/concurrency.async-sequence-on-self
memorydetective://patterns/webkit.wkscriptmessagehandler-bridge
memorydetective://patterns/swiftdata.modelcontext-actor-cycle
…
```
`resources/list` retorna todas as 34 entradas. `resources/read` resolve qualquer URI `memorydetective://patterns/{id}` no seu body markdown.
### Prompts (7)
Playbooks de investigação são expostos como prompts MCP (slash commands em clients que expressam eles, ex: Claude Code).
| Slash command | O que faz | Args |
|---|---|---|
| `/investigate-leak` | Roda a investigação canônica de 6 steps memgraph-leak: `analyzeMemgraph` → `classifyCycle` → `reachableFromCycle` → `swiftSearchPattern` → `swiftGetSymbolDefinition` → `swiftFindSymbolReferences`. | `memgraphPath` |
| `/investigate-hangs` | Diagnostica hangs main-thread visíveis ao usuário a partir de um `.trace`. | `tracePath` |
| `/investigate-jank` | Diagnostica frames droppados / animation hitches a partir de um `.trace`. | `tracePath` |
| `/investigate-launch` | Diagnostica slowness de launch cold/warm a partir de um `.trace`. | `tracePath` |
| `/verify-cycle-fix` | Diff de um par before/after de snapshots `.memgraph` pra confirmar que um fix passou. | `before`, `after` |
| `/summarize-trace` | Summary card cross-schema single-call pra um `.trace`. Envolve `summarizeTrace`. v1.13+. | `tracePath` |
| `/investigate-metrickit` | Fluxo post-mortem pra payloads Apple MetricKit `.mxdiagnostic` de builds TestFlight / App Store. Envolve `analyzeMetricKitPayload` com prioridade de leitura crashCluster + hangHotspots + cpuExceptions + diskWriteExceptions + cross-tool chain hints (objc_release → findCycles, sqlite → analyzeHangs). v1.18+. | `payloadPath` |
Cada prompt preenche os templates de argumento do playbook canônico com os valores fornecidos pelo user, e depois entrega ao agente um brief pronto-pra-executar. Chama as mesmas ferramentas listadas em [Ler & analisar](#ler--analisar-14). Prompts são um atalho de orquestração, não um engine separado.
### Modo CLI
O mesmo binário também é um CLI fino pra scripting e CI:
```bash
memorydetective analyze # totais, ROOT CYCLEs, diagnóstico
memorydetective classify # casa patterns + renderiza fix hint
memorydetective tool --input # dispatcher genérico pra qualquer ferramenta MCP
memorydetective --help
memorydetective --version
```
Quando chamado sem argumentos, o binário inicia como server MCP sobre stdio.
O subcommand `tool` despacha pra qualquer ferramenta MCP registrada pelo nome, lendo inputs de um arquivo JSON. Exit code é `0` quando a ferramenta retorna `ok && passed !== false`, `1` caso contrário, pra que encaixe limpo em gates de CI. Nomes de ferramenta atualmente suportados: `detectLeaksInXCTest`, `detectLeaksInXCUITest` (a [receita de CI](#adicione-memorydetective-no-seu-ci-em-5-minutos) acima usa isso).
---
## Requisitos
- macOS com Xcode Command Line Tools (`xcode-select --install`)
- Node.js ≥ 20
## Desenvolver
```bash
git clone https://github.com/carloshpdoc/memorydetective
cd memorydetective
npm install
npm test # 758 unit tests
npm run build # build → dist/
npm run dev # tsx, modo stdio (dev mode)
./scripts/demo.sh # demo completa contra um .memgraph real (defina MEMGRAPH=path)
```
## Contribuir
Contribuições são bem-vindas. Bug reports, feature requests, novos patterns de ciclo, qualquer um.
- **Bugs / feature requests**: [abrir issue](https://github.com/carloshpdoc/memorydetective/issues).
- **PRs**: fork → branch → `npm install` → faz mudanças → `npm test` (758 testes têm que continuar green) → abre PR com descrição concisa do que mudou e por quê.
### Adicionando um pattern de ciclo no `classifyCycle`
`classifyCycle` shipa com 36 patterns built-in cobrindo SwiftUI (incl. Swift 6 / `@Observable` / SwiftData / NavigationStack / as formas v1.9 `observable-write-on-every-render` e `viewcontroller-retained-after-pop`), Combine, Swift Concurrency (incl. AsyncSequence-on-self e `Observations`), UIKit (Timer / CADisplayLink / UIGestureRecognizer / KVO / URLSession / WebKit / DispatchSource), Core Animation, Core Data, pattern Coordinator, RxSwift, e Realm. Pra adicionar um:
1. Edita `src/tools/classifyCycle.ts`. Adiciona uma entry em `PATTERNS` com `id`, `name`, `fixHint`, e uma função `match`.
2. Adiciona um teste em `src/tools/readTools.test.ts` que assert que o novo pattern dispara contra um fixture representativo de memgraph.
3. Adiciona uma entry `staticAnalysisHint` em `src/runtime/staticAnalysisHints.ts` (o teste nesse arquivo enforça cobertura 1:1 com `PATTERNS`).
4. Adiciona uma entry `fixTemplate` em `src/runtime/fixTemplates.ts` (mesmo guard de cobertura 1:1).
5. Abre um PR.
## Apoie esse projeto
Se `memorydetective` te economiza tempo, você pode apoiar o desenvolvimento continuado:
- ☕ [Buy me a coffee](https://buymeacoffee.com/carloshperc)
- 💖 [Sponsor no GitHub](https://github.com/sponsors/carloshpdoc)
Toda contribuição ajuda a manter isso mantido e documentado.
## Licença
Apache 2.0. Veja [LICENSE](./LICENSE) e [NOTICE](./NOTICE).
Permite uso comercial, modificação, distribuição, uso de patent. Inclui cláusula de atribuição via o arquivo `NOTICE`.
## Por que "memorydetective"?
Caçar retain cycles em SwiftUI parece trabalho de detetive: você tem um corpo (a instância vazada), uma cena do crime (o `.memgraph`), e uma cadeia de suspeitos (a retain chain). A ferramenta te ajuda a ler a evidência e nomear o assassino. A marca segue o trabalho.