# Investigate

> Routes a code investigation to one of four modes — bug, test gap, architecture, implementation — and runs the query sequence that mode needs. Use when asked to investigate, trace, or debug code.

- Skill: `madappgang/investigate-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add madappgang/investigate-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/madappgang/investigate-2/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: MadAppGang (https://skillmd.com/u/madappgang)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/madappgang/investigate-2

---


# Investigate

One investigation, one mode. Pick the mode from the request, announce it, then run that
mode's sequence. The retrieval mechanics live in the `code-search` skill; this skill is
about which questions to ask and in what order.

## Routing

| Mode | Trigger keywords | Question sequence |
|---|---|---|
| **Bug** | debug, error, broken, failing, crash | full context → inbound → outbound → transitive impact |
| **Test gap** | test, coverage, edge case, mock | inbound filtered to test files → gap detection |
| **Architecture** | architecture, design, structure, layer | structural map → centrality → dependency closure |
| **Implementation** *(default)* | how does, implementation, data flow | locate → inbound → outbound → full context |

**Collision order: Bug > Test > Architecture > Implementation.** No keyword matches →
Implementation.

**Announce the routing before executing it** — print the query, the chosen mode, and the
one-line reason. A wrong route is then correctable in one turn instead of after the
investigation is spent.

## Discipline that applies to every mode

- **Never rank-truncate a result set.** Ranking already put the important results first;
  cutting by line count deletes them. Narrow with `scope` instead.
- **An error and an empty result look alike and mean opposite things.** Establish which one
  you have before you report either.
- **Name the method** behind every finding. Lexical search is the right tool for exact
  strings, counts and filename patterns — using it is routing, not a fallback. Using it
  without saying so is the defect.
- **Blocked, never stalled.** A subagent has no tool for asking the user a question. If you
  cannot proceed, do not wait for an answer that cannot arrive and do not decide on the
  user's behalf: return a result beginning `BLOCKED:` naming what is missing and what would
  unblock it, and let the dispatching orchestrator ask.
- **Centrality is relative.** Treat it as tiers — core, key, ordinary, leaf — never as a
  number to threshold. An absent centrality means unknown, not low.

---

## Architecture mode

**Use when:** "what is the architecture", "how are the layers structured", "find the design
patterns", "map the system".

The highest-centrality symbols *are* the architecture. Start there, and skip the leaves.

**Find the layers** by running the same structural query against three vocabulary families
and reading the file distribution of the results:

```
code_search(query: "controller handler endpoint route")   # presentation
code_search(query: "service business logic domain")       # business
code_search(query: "repository persistence database query") # data
```

Boundaries and wiring show up under `interface contract abstract`, `inject provider module`
and `config bootstrap initialize`.

**Find the patterns** the same way — the pattern names are in the source:

```
code_search(query: "factory create builder")
code_search(query: "interface abstract contract")
code_search(query: "event emit subscribe")
code_search(query: "repository persist unit of work")
```

**Then take the closure.** Direct callers are one level; the blast radius of an
architectural change is the transitive closure, grouped by depth.

```
find_dependents(symbol: "PaymentService")
impact(symbol: "PaymentService", max_depth: 3)
```

**Report:** detected pattern, core abstractions with their centrality tier and file:line,
the layer diagram, the major flows, and the health indicators.

### Persist what you derived

Architecture knowledge is expensive to re-derive and cheap to record. Write it down once
derived — as bullet summaries plus `file:line` pointers, **never pasted code**. Code goes
stale on the next edit; pointers survive it.

---

## Implementation mode (default)

**Use when:** "how does X work", "trace the data flow", "where is X defined".

1. **Locate** the symbol — exact span, kind, signature, export status.
2. **Inbound edges** — every place that calls it. This is the impact of changing it.
3. **Outbound edges** — everything it calls. These are its dependencies and the data-flow
   path.
4. **Full context** when the change is non-trivial: definition plus both directions at once.

Edge kind matters as much as edge existence: `call`, `import`, `extends` and `implements`
have different blast radii. Say which kind an edge is when it changes the conclusion.

**Direct callers are one level. `impact` is the transitive closure.** Refactoring decisions
need the closure; a direct-caller count under-states the work every time.

**Before editing:** confirm the symbol resolves, confirm its current signature, think, then
edit. Never grep → read → edit. Index-derived signatures are captured at index time and can
lag the file — when the name is overloaded or generic, read the declaration at the returned
span rather than trusting the summary.

**Report:** primary location with `file:line`, inbound edges, outbound edges, and the
end-to-end flow.

---

## Test gap mode

**Use when:** "what is tested", "find the coverage gaps", "audit test quality", "missing
tests", "edge cases".

Tests appear in the graph as **callers of the code they test**. Zero test callers on code
that has production callers is the high-priority case.

1. Take the inbound edges of each critical symbol.
2. Classify each caller as test or production by path, using one pattern set:

   ```
   *.test.*  *.spec.*  __tests__/  tests/
   *_test.go  *_test.py  test_*.py  src/test/java/  *_test.rs
   ```

3. **Report both counts.** Production callers > 0 and test callers = 0 is the finding worth
   escalating; low-centrality untested code is not a gap worth reporting.

**The convention is incomplete, and the verdict must say so.** Filename matching misses
integration tests in non-standard locations, inline test modules (Rust `#[cfg(test)]`),
end-to-end suites that reach the code indirectly, and any language whose convention is not
in the list above. A "0 test callers" result from filename matching alone is a hypothesis,
not a coverage verdict.

**Two methods will disagree, and that is information.** Mock registration (`vi.mock`,
`jest.mock`) and generated cases (`describe.each`) are call sites that AST edges miss but a
reference-level lookup finds. When the two counts differ, report both rather than picking
one.

**Report:** framework, test file count, a per-function table of test callers, then the
high-priority list (production callers, no test callers) and the medium list (few test
callers, no error scenarios).

---

## Bug mode

**Use when:** "why is X broken", "find the bug source", "root cause analysis", "trace this
error".

**Trace backwards through callers to find the cause; trace forwards through callees to find
the effect.** The full chain is the root-cause picture.

1. Locate the symbol named in the error, and take its full context.
2. Walk inbound edges outward — caller of the symbol, caller of that caller — until the
   chain reaches something that explains the symptom.
3. Walk outbound edges to see what the failure propagates into.
4. Take the transitive impact for post-fix verification and regression scope.

**Check the type boundary explicitly.** Mismatches between a symbol's actual signature and
what its callers assume are a leading cause of runtime errors, and they are invisible in a
call graph that only records that an edge exists. Read the declaration and read one caller.

**Error origin hunting:** map the error vocabulary (`throw error exception`), locate the
error type, then take *its* inbound edges — whoever constructs it is where the condition is
detected.

**State mutation tracking:** locate the mutator (`set state update mutate`), then enumerate
who calls it.

**Report:** symptom → the backwards call chain → root cause with `file:line` → the evidence
that establishes it → the count of affected locations. Every step carries its evidence; a
root cause asserted without a chain is a guess.

---

## Reading "0 callers"

**Zero inbound edges has three readings, and they are not interchangeable:**

1. **Entry point** — expected and correct. CLI mains, HTTP handlers, exported public API,
   test-only helpers.
2. **Dead code** — genuinely unreachable.
3. **A call the graph cannot see** — dynamic import, reflection, bracket dispatch, event or
   callback registration, dependency-injection wiring, or a consumer in another repository.

**Never collapse the three.** Report which one you concluded and the evidence for it.

**This skill does not authorise deletion.** A dead-code verdict needs all three of: zero
inbound edges, low centrality, and **not exported** — an export is a public contract whose
consumers may be outside this tree. Even with all three, the finding is labelled *requires
manual review* until a human has checked it against the dynamic-dispatch list above.

**High centrality with zero callers means something broke recently** — a deleted call site,
a botched merge — and is an investigation, not a cleanup. Low centrality with zero callers
is a cleanup *candidate*, which is a proposal, not a verdict.

