Walkthroughs

Examples

Five questions a static search cannot answer, answered on real fixtures in Java, Python, and TypeScript. Every response below is actual tool output.

Setting

Three fixtures, one schema

Each language ships a six-file fixture built from the same shapes — a service, a repository, a route, an injected dependency, and one run-time lookup. The Java fixture goes deepest on dispatch and advice; the Python and TypeScript fixtures cover framework wiring.

Fixture Path Crawl result Exercises
Java tests/fixtures/java/dynamic 6 files · 43 nodes · 61 edges interface dispatch, AspectJ advice, decorators
Python tests/fixtures/python/sample 6 files · 49 nodes · 65 edges FastAPI routes, Depends() DI, importlib
TypeScript tests/fixtures/typescript/sample 6 files · 62 nodes · 81 edges NestJS + Express routes, DI, dynamic import()

The Java fixture is reproduced below and drives examples 1–5. The Python and TypeScript equivalents appear in the per-language section that follows.

Payment.java · Checkout.java
public interface Payment {
    void pay(double amount);
}

public class Checkout {
    private final Payment payment;

    public Checkout(Payment payment) {
        this.payment = payment;
    }

    public void checkout(double amount) {
        payment.pay(amount);   // which pay()?
    }
}
AuditAspect.java · DecoratedService.java
@Aspect
public class AuditAspect {
    @Pointcut("execution(* com.acme.*.pay(..))")
    public void paymentCall() {}

    @Around("paymentCall()")
    public Object auditPayment(JoinPoint jp) { ... }

    @Before("execution(* com.acme.Checkout.checkout(..))")
    public void beforeCheckout() {}
}

@Service
public class DecoratedService {
    @Transactional(readOnly = true)
    @Cacheable("orders")
    public String describe(String id) { ... }
}

Index it with crawl_project({ path: ".../tests/fixtures/java/dynamic" }). The crawl reports 6 files, 43 nodes, 61 edges, 2 dynamic-dispatch edges, and 4 advice edges. Response bodies below are trimmed to the fields that matter.

Same tools, any language

Per-language examples

Tool names and response shapes do not change with the language. What changes is what each grammar lets the parser see — so these three fixtures are chosen to show different surfaces. Responses are actual output with filePath values trimmed.

Python · FastAPI routes and Depends() wiring

app/api.py · app/service.py
router = APIRouter()

@router.get("/orders/{order_id}")
def read_order(order_id: str,
               service: OrderService = Depends(get_service)):
    return service.repo.find(order_id)

@router.post("/orders")
def create_order(...):
    return {"id": service.place(order_id, amount)}

@injectable
class OrderService:
    def __init__(self, repo: OrderRepository) -> None:
        self.repo = repo
find_routes()
{
  "source": { "id": "module:app.api", "kind": "module" },
  "target": { "id": "module:app.api#read_order" },
  "framework": "router",
  "httpMethod": "GET",
  "path": "/orders/{order_id}",
  "line": 13
},
{
  "source": { "id": "module:app.api" },
  "target": { "id": "module:app.api#create_order" },
  "framework": "router",
  "httpMethod": "POST",
  "path": "/orders",
  "line": 18
}
find_injections()
// constructor injection
app.service.OrderService
  INJECTS app.repository.OrderRepository
  framework: "dependency-injector"
  injectedName: "repo" · line 15

// framework-supplied parameter
module:app.api
  INJECTS app.service.OrderService
  framework: "fastapi"
  injectedName: "service" · line 19
find_reflections() · find_entry_points()
// importlib.import_module("app.plugins." + name)
module:app.dynamic#load_plugin
  REFLECTS __reflect__:app.plugins.
  mechanism: "importlib.import_module" · line 6

// entry points — CLI and HTTP in one index
module:app.main            cli   __main__
module:app.main#main       cli   main
module:app.api#create_order http POST /orders
module:app.api#read_order   http GET /orders/{order_id}

trace_execution("app.service.OrderService#place") reaches app.repository.OrderRepository#create (line 19) and app.service.OrderService#audit (line 20) at depth 1, then stops at the boundary ext:builtins. The framework field on a route reports the decorator receiver — router here, because the handler hangs off an APIRouter instance — while a Depends() parameter reports fastapi.

TypeScript · one endpoint, two frameworks

src/routes.ts · src/controller.ts
const app = express();
app.get("/orders/:id", (req, res) => {
  res.json(service.place(req.params.id));
});

@Controller("orders")
export class OrderController {
  constructor(private readonly service: OrderService) {}

  @Get(":id")
  read(@Param("id") id: string) {
    return this.service.place(id);
  }
}
find_routes()
// Express — inline handlers get synthetic ids
module:src.routes
  ROUTES module:src.routes#<anonymous@7>
  framework: "express" · GET /orders/:id · line 6

// NestJS — decorators fold controller + method paths
src.controller.OrderController
  ROUTES #read    framework: "nestjs"
  GET /orders/:id · line 7
src.controller.OrderController
  ROUTES #create  framework: "nestjs"
  POST /orders · line 12

4 routes · 2 frameworks · 1 logical resource
find_injections() · find_reflections()
src.controller.OrderController
  INJECTS src.service.OrderService
  framework: "ts-di" · injectedName: "service"

src.service.OrderService
  INJECTS src.repository.OrderRepository
  framework: "ts-di" · injectedName: "repo"

// await import(`./plugins/${name}`)
module:src.dynamic#loadPlugin
  REFLECTS __reflect__:./plugins/${name}
  mechanism: "import()" · line 1
trace_execution("src.service.OrderService#place")
depth 1  OrderRepository#create  repository.ts:8
depth 1  OrderService#audit       service.ts:9
depth 2  OrderRepository#find     repository.ts:9
depth 2  ext:console              external boundary
depth 2  __unknown__:this.rows.push receiver type unknown

// unresolved calls stay visible instead of vanishing —
// widen the call with get_node() and try again.

Java · which override runs, and what wraps it

find_implementations("com.acme.Payment")
com.acme.Payment
  IMPLEMENTS com.acme.CardPayment   depth 1
  IMPLEMENTS com.acme.UpiPayment    depth 1

// find_routes() · find_injections()
[]  no web framework, no DI container

2 overrides · every target a Payment call can reach
find_aspects()
com.acme.AuditAspect
  auditPayment    around    paymentCall()
    ADVISES CardPayment#pay · Payment#pay · UpiPayment#pay
  beforeCheckout  before    execution(* com.acme.Checkout.checkout(..))
    ADVISES Checkout#checkout
  paymentCall     pointcut  execution(* com.acme.*.pay(..))
    named pointcut — resolves to 3 targets, advises none itself
trace_execution("com.acme.Checkout#checkout")
depth 1  AuditAspect#beforeCheckout   INTERCEPT · before
depth 1  Payment#pay                  CALLS · Checkout.java:11
depth 2  AuditAspect#auditPayment     INTERCEPT · around
depth 2  CardPayment#pay              DYNAMIC_DISPATCH
depth 2  UpiPayment#pay               DYNAMIC_DISPATCH
depth 3  CardPayment#authorize        CardPayment.java:6
depth 3  UpiPayment#settle            UpiPayment.java:6
depth 3  __unknown__:joinPoint.proceed AuditAspect.java:17
depth 4  __unknown__:System.out.println UpiPayment.java:10

No web framework and no DI container in this fixture, so find_routes() and find_injections() return [] — the honest answer rather than a guess. What it exercises is the harder question: which override actually runs, and which advice wraps it. The five walkthroughs below use this fixture.

Example 1

Impact analysis: who reaches this method?

Before changing Payment#pay, ask for every caller — including virtual-dispatch and advice callers that a text search misses.

request
get_callers({
  symbol: "com.acme.Payment#pay",
  transitive: true,
  include_dynamic: true,
  include_advice: true
})
response
{
  "target": {
    "id": "com.acme.Payment#pay",
    "kind": "method",
    "filePath": "com/acme/Payment.java"
  },
  "callers": [
    {
      "caller": {
        "id": "com.acme.AuditAspect#auditPayment",
        "kind": "method"
      },
      "depth": 1,
      "edgeKind": "ADVISES"
    },
    {
      "caller": {
        "id": "com.acme.Checkout#checkout",
        "kind": "method"
      },
      "depth": 1,
      "callSiteLine": 11,
      "edgeKind": "CALLS"
    },
    {
      "caller": {
        "id": "com.acme.AuditAspect#beforeCheckout",
        "kind": "method"
      },
      "depth": 2,
      "edgeKind": "ADVISES"
    }
  ]
}

Both advice callers are the point. AuditAspect#auditPayment never mentions pay in its own body — it inherits the dependency from a named pointcut. beforeCheckout only reaches pay at depth 2, through its advised caller. A text search finds neither.

Example 2

Runtime trace: what actually executes?

trace_execution follows static calls, then fans out across virtual dispatch and weaves advice in as INTERCEPT hops.

request · trace_execution
trace_execution({
  symbol: "com.acme.Checkout#checkout",
  include_dynamic: true,
  include_advice: true,
  max_depth: 6
})
response · steps
{
  "root": { "id": "com.acme.Checkout#checkout", "kind": "method" },
              "stepCount": 9,
  "steps": [
    {
                  "symbol": { "id": "com.acme.AuditAspect#beforeCheckout" },
                  "depth": 1,
                  "via": "INTERCEPT"
                },
                {
                  "symbol": { "id": "com.acme.Payment#pay" },
                  "depth": 1,
                  "via": "CALLS",
                  "callSiteLine": 11
                },
                {
                  "symbol": { "id": "com.acme.AuditAspect#auditPayment" },
                  "depth": 2,
                  "via": "INTERCEPT"
    },
    {
                  "symbol": { "id": "com.acme.CardPayment#pay" },
      "depth": 2,
                  "via": "DYNAMIC_DISPATCH"
    },
    {
                  "symbol": { "id": "com.acme.UpiPayment#pay" },
      "depth": 2,
      "via": "DYNAMIC_DISPATCH"
    },
    {
                  "symbol": { "id": "__unknown__:joinPoint.proceed" },
                  "depth": 3,
                  "via": "CALLS",
                  "callSiteLine": 17
                },
                {
                  "symbol": { "id": "com.acme.CardPayment#authorize" },
                  "depth": 3,
                  "via": "CALLS",
                  "callSiteLine": 6
                },
                {
                  "symbol": { "id": "com.acme.UpiPayment#settle" },
                  "depth": 3,
                  "via": "CALLS",
                  "callSiteLine": 6
                },
                {
                  "symbol": { "id": "__unknown__:System.out.println" },
                  "depth": 4,
                  "via": "CALLS",
                  "callSiteLine": 10
                }
              ]
            }

Reading the via field is the whole point: CALLS is what the source says, DYNAMIC_DISPATCH is what the hierarchy allows at runtime, and INTERCEPT is behaviour injected by an aspect.

Example 3

Aspect audit: what intercepts what?

Pointcuts are resolved — named pointcut references, execution, wildcards, and boolean operators all produce concrete ADVISES edges.

request
find_aspects({ limit: 20 })
response
{
  "aspectCount": 1,
  "aspects": [
    {
      "aspect": { "id": "com.acme.AuditAspect" },
      "advice": [
        {
          "method": { "id": "com.acme.AuditAspect#auditPayment" },
          "adviceKind": "around",
          "pointcut": "paymentCall()",
          "advised": [
            { "id": "com.acme.CardPayment#pay" },
            { "id": "com.acme.Payment#pay" },
            { "id": "com.acme.UpiPayment#pay" }
          ]
        },
        {
          "method": { "id": "com.acme.AuditAspect#beforeCheckout" },
          "adviceKind": "before",
          "pointcut": "execution(* com.acme.Checkout.checkout(..))",
          "advised": [
            { "id": "com.acme.Checkout#checkout" }
          ]
        },
        {
          "method": { "id": "com.acme.AuditAspect#paymentCall" },
          "adviceKind": "pointcut",
          "pointcut": "execution(* com.acme.*.pay(..))",
          "advised": []
        }
      ]
    }
  ]
}

@Around("paymentCall()") is reported with the resolved effect — all three pay methods — not just the pointcut name. The named pointcut itself appears with an empty advised list: it declares a predicate, it does not intercept anything. This is the difference between indexing an annotation and understanding it.

Example 4

Decorators: runtime semantics attached to code

Annotations are graph nodes, so you can find every symbol carrying one — or list everything attached to a single type and its members.

find_annotated
find_annotated({
  annotation: "Transactional",
  kind: "method"
})
response
{
  "annotation": "Transactional",
  "count": 1,
  "results": [
    {
      "symbol": {
        "id": "com.acme.DecoratedService#describe",
        "kind": "method",
        "filePath": "com/acme/DecoratedService.java"
      },
      "annotation": "Transactional",
      "line": 9,
      "arguments": ["readOnly = true"]
    }
  ]
}
get_decorators · include_members
get_decorators({
  symbol: "com.acme.DecoratedService",
  include_members: true
})

// decorators: [ { name: "Service", decorator: false } ]
// members[0].symbol.id: "com.acme.DecoratedService#describe"
// members[0].decorators:
//   { name: "Cacheable",     decorator: true, arguments: ["\"orders\""] }
//   { name: "Transactional", decorator: true, arguments: ["readOnly = true"] }
// string literals keep their quotes — that is the source, not a JSON artifact

decorator: true marks framework annotations whose semantics are behavioural — caching, transactions, retries, async execution, security. Structural annotations such as @Override or @Service are indexed but not flagged.

Example 5

Hotspots and dead-code candidates

Two ends of the same graph: where the traffic concentrates, and what nothing calls.

find_hotspots · min_fan_in: 1
{
  "count": 5,
  "hotspots": [
    {
      "symbol": { "id": "com.acme.Payment#pay" },
      "fanIn": 1,
      "fanOut": 2,
      "adviceCount": 1
    },
    {
      "symbol": { "id": "com.acme.CardPayment#authorize" },
      "fanIn": 1,
      "fanOut": 1,
      "adviceCount": 0
    },
    {
      "symbol": { "id": "com.acme.CardPayment#pay" },
      "fanIn": 1,
      "fanOut": 1,
      "adviceCount": 1
    },
    {
      "symbol": { "id": "com.acme.UpiPayment#pay" },
      "fanIn": 1,
      "fanOut": 1,
      "adviceCount": 1
    },
    {
      "symbol": { "id": "com.acme.UpiPayment#settle" },
      "fanIn": 1,
      "fanOut": 1,
      "adviceCount": 0
    }
  ]
}
find_dead_code
{
  "count": 0,
  "unused": []
}

That empty result is correct, not a bug: the fixture lives under tests/, and test sources are excluded by default. Re-running with include_tests: true and include_constructors: true surfaces five candidates — including AuditAspect#auditPayment, which is reached by the framework, not by any call. ADVISES edges point from advice to the advised method, so nothing points at the advice itself. Treat every row as a candidate, never a verdict.

Composing the primitives

Real questions combine tools. “Is this aspect safe to remove?” becomes find_aspects → get_callers on each advised method → trace_path from the application entry point. “What breaks if I change this interface?” becomes find_implementations → get_callers per override → find_annotated to spot transactional or cached implementations that carry extra contracts.

Every tool, every argument.

The reference lists the complete input schema and response shape for all 22 tools.

Tool reference →