Skip to content
Back to skills

Wf Explore Code Relations

ASecurity

Walk a codebase with the call graph tools: callers, impact, neighbors.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 19, 2026
ai-agentstypescriptgophpbashvuenodetestingdebugginggitapi

Works with

  • api
  • mcp

Security analysis

A100/100

Scanned October 19, 2026

npx -y skills add Project-SandStar/FantomMcpServer --skill wf-explore-code-relations --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Wf Explore Code Relations?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Wf Explore Code Relations
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/project-sandstar-wf-explore-code-relations/badge)](https://www.skillsdirectory.com/skills/project-sandstar-wf-explore-code-relations)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: wf-explore-code-relations
description: "Walk a codebase with the call graph tools: callers, impact, neighbors."
---

<!-- generated from workflows/explore-code-relations.md by claudePlugin/scripts/build.mjs; edit the source, not this file -->

# Explore Code Relations via the Fantom MCP Server

> **Project of truth:** `SandStar Website` (project id `267`, language `php`,
> 102,575 code nodes, 232k+ `calls` edges). All examples below were executed
> live against this project on 2026-04-15 and their responses are the
> documented expectation.

## Purpose

Teach an AI how to stand at a single symbol in a project and expand outward
to understand how the codebase is wired together. The graph lives in
LadybugDB (authoritative store since 2026-04-15); the MCP server exposes it
through ten tools. The workflow below is the full exploration loop.

---

## Tool map

| MCP Tool | Reads from | Works on |
|---|---|---|
| `listFantomProjects` | Prisma `FantomProject` | every project |
| `getFantomCodeStats` | in-memory `FantomCodeIndexer` | **Fantom-language only** |
| `getFantomFunction` | in-memory `FantomCodeIndexer` | **Fantom-language only** |
| `getFantomType` | in-memory `FantomCodeIndexer` | **Fantom-language only** |
| `searchFantomCode` | FlexSearch index over the in-memory indexer | **Fantom-language only** |
| `semanticCodeSearch` | LanceDB vectors + Ladybug | every project |
| `findSimilarCode` | LanceDB vectors + Ladybug | every project |
| `getCallers` | **Ladybug** (Cypher) | every project |
| `getCallees` | **Ladybug** (Cypher) | every project |
| `getCodeImpact` | **Ladybug** (Cypher) | every project |
| `reindexChangedFiles` | parser + Ladybug + Prisma | every project (used after edits) |
| `listIndexRuns` | Prisma `IndexRun` | every project |
| `getApiChangeHistory` | Prisma `ApiChange` | every project |
| `diffIndexRuns` | Prisma `ApiChange` | every project |

**Rule of thumb:** the four graph-native tools (`getCallers`, `getCallees`,
`getCodeImpact`, plus `semanticCodeSearch` and `findSimilarCode`) are the
universal ones. For PHP/TypeScript/Vue/etc. projects stay on these.

The bottom four (`reindexChangedFiles`, `listIndexRuns`,
`getApiChangeHistory`, `diffIndexRuns`) close the read → edit → re-read
loop. After any Edit / Write the AI performs, call `reindexChangedFiles`
so the next exploration sees the fresh state.

---

## Step 1 — Anchor: pick a starting symbol

For a known Fantom project, use keyword search:

```bash
# Always use the MCP tool at call-time; curl is shown for the shape only.
curl -s -u admin:admin -H 'Content-Type: application/json' \
  'http://localhost:3848/admin/code-stats' | jq '.totalProjects, .totalFunctions'
```

For a non-Fantom project (or when the concept is fuzzy), use
`semanticCodeSearch` with `projectId` set:

```json
{
  "tool": "semanticCodeSearch",
  "args": { "query": "handle OAuth registration requests",
            "projectId": 267, "limit": 5 }
}
```

Verified response on project 267 returns 5 hits led by
`OAuthRegisterController` (semantic 71.9 %).

---

## Step 2 — Expand outward: who calls the symbol?

`getCallers` is the primary "where is this used?" answer.

```json
{ "tool": "getCallers",
  "args": { "qualifiedName": "SandStar Website::Str",
            "projectId": 267, "maxDepth": 1 } }
```

Verified: 219 depth-1 callers, capped at `maxDepth=1`. Depth 2–5 reveal
indirect users but grow combinatorially; start at 1 and widen only if
needed.

Behaviour notes learned in testing:
- Results are **deduplicated by node id** — a function that calls your
  target 10 times shows once with the minimum depth.
- `projectId` scopes the lookup; omit it to see cross-project callers
  (useful for shared libraries).
- Edge types included: `calls` + `uses`. Inheritance and containment are
  excluded here; use `getCodeImpact` for the full set.

---

## Step 3 — Descend: what does the symbol call?

```json
{ "tool": "getCallees",
  "args": { "qualifiedName": "SandStar Website::OAuthRegisterController.__invoke",
            "projectId": 267, "maxDepth": 2 } }
```

Verified response: 7 callees covering `Str`, `Validator`, `collect`,
`config`, `response`, `Container`, and `app` (depth 2 via `response → app`).

Use `getCallees` with depth 2–3 to understand the **dependency surface** of
a function before editing it. Unlike `getCallers`, this is a forward walk;
edges traversed are `calls` only.

---

## Step 4 — Blast radius: change-impact analysis

`getCodeImpact` is `getCallers` on steroids — it widens the edge filter to
`calls, extends, implements, uses, returns, parameters`, which captures
inheritance and type relationships a pure call-graph would miss.

```json
{ "tool": "getCodeImpact",
  "args": { "qualifiedName": "SandStar Website::Str",
            "projectId": 267, "maxDepth": 2 } }
```

Verified: 916 affected nodes, split `calls: 925` + `returns: 706` (some
nodes accumulate across multiple edge types, which is why the totals are
larger than the distinct count).

Use this before a rename or signature change.

> For a leaf function that nothing depends on (`__invoke` entry points,
> HTTP controllers) `getCodeImpact` returns `0`. That is not a bug — the
> function is at the graph's edge. Move up one hop with `getCallers` to
> find the closest hub.

---

## Step 5 — Lateral search: "find me something similar"

Two modes:

### 5a. Semantic search (question-form)

```json
{ "tool": "semanticCodeSearch",
  "args": { "query": "validate email formatting and reject disposable domains",
            "projectId": 267, "limit": 5 } }
```

Returns scored results with combined semantic + graph-context signals.

### 5b. Function-similarity (give me more like this one)

```json
{ "tool": "findSimilarCode",
  "args": { "qualifiedName": "SandStar Website::OAuthRegisterController.__invoke",
            "projectId": 267, "limit": 3 } }
```

Verified response: `OAuthRegisterController`, `LoginController.oauthProviders`,
`PrismModelController.__invoke` at 77.6 % / 76.8 % / 76.7 % similarity. Use
when hunting for duplicate logic or pre-existing patterns.

---

## Step 6 — Close the loop: detailed symbol inspection

For Fantom projects, grab the full source + signature + facets:

```json
{ "tool": "getFantomFunction",
  "args": { "qualifiedName": "bassgPointHealth::Build.publish" } }
```

Verified response includes `sourceCode`, `signature`, `parameters`,
`returnType`, `facets`, `filePath`, `lineNumber`.

For **non-Fantom projects** this tool returns `Function not found`. Use
`getCallers` / `findSimilarCode` / `semanticCodeSearch` instead — they
return `filePath` and `lineStart`, which is enough to open the file.

---

## Step 7 — Edit code and keep the index in sync

**The MCP server does not auto-watch the filesystem.** After every batch
of Edit / Write tool calls, the assistant is responsible for telling the
server which files changed so the next query reflects the new state.

```json
{ "tool": "reindexChangedFiles",
  "args": {
    "paths": [
      "~/Code/court-lens-mcp/src/core/audioRouter.ts",
      "~/Code/court-lens-mcp/src/core/mixer.ts"
    ]
  }
}
```

Rules:

- Pass **absolute** paths.
- Pass **all** files touched in the same logical edit unit; one call per
  unit, not per file. The server batches them into a single `IndexRun`
  with one diff snapshot.
- Omit `projectId` when paths obviously fall under a single project — the
  server resolves it via the longest matching `project.path` prefix.
- If your edits span multiple projects, call once per project (the server
  refuses ambiguous batches with a structured error).
- This call is cheap: typically <1s for a handful of files vs. ~100s for
  a full `refreshFantomProject`. Use liberally.

Response shape (verified):

```json
{ "success": true, "runId": 42, "projectId": 268,
  "filesProcessed": 2, "added": 1, "modified": 7, "removed": 1,
  "durationMs": 820, "warnings": [], "errors": [] }
```

Verify the edit landed:

```json
{ "tool": "searchFantomCode",
  "args": { "query": "dispatchFrame", "projectId": 268 } }
```

### Edge cases

- **File deleted** — include the path in `paths`; the server records
  `changeType: "removed"` for every symbol that was in it and drops the
  `IndexedFile` row.
- **File renamed** — v1 surfaces this as remove + add (no rename
  tracking). Pass both old and new paths.
- **Cross-file edge dangling** — per-file rebuilds don't touch other
  files' outbound edges, so a caller in `playbackHandler.ts` referring
  to a renamed symbol may show as unresolved until that file is also
  re-indexed. **If you renamed a symbol, always include its callers in
  `paths`.**

### When to fall back to a full refresh

Use `refreshFantomProject({ projectName })` instead of
`reindexChangedFiles` when:

- A `git checkout` / `git pull` rewrote dozens of files at once.
- The user reports stale graph data (cross-file edges look wrong).
- You're debugging an indexer issue and want a known-good baseline.

`refreshFantomProject` with `force: true` skips the per-file hash gate.

---

## Step 8 — Drift detection: "what happened to X?"

Once edits land, the `IndexRun` and `ApiChange` history lets you answer
questions about the past. Use these when:

- The user says "this function used to exist."
- A search returns nothing where it used to return something.
- You need to summarise "what changed in this project today / this week."

```json
{ "tool": "listIndexRuns",
  "args": { "projectId": 268, "since": "2026-04-28T00:00:00Z", "limit": 50 } }
```

```json
{ "tool": "getApiChangeHistory",
  "args": { "qualifiedName": "core::AudioRouter.routeFrame" } }
```

```json
{ "tool": "diffIndexRuns",
  "args": { "projectId": 268,
            "fromTime": "2026-05-04T00:00:00Z",
            "toTime":   "2026-05-05T23:59:59Z" } }
```

`diffIndexRuns` returns
`{ summary: { added, modified, removed }, added: [...], modified: [...], removed: [...] }`
with each entry carrying `beforeSig` + `afterSig` so you can summarise
the API delta in prose.

Caveats when interpreting history:

- Runs marked `isSeedingRun: true` count everything as "added" because
  there was no prior state. Skip those when computing trends.
- `modified` detection compares the LadybugDB `signature` field. Body-only
  edits that don't change the signature won't show up — read the source
  if the user asks "did the implementation change?".
- Renames are remove + add, not modify. A function that disappears and
  a similar one that appears in the same `IndexRun` is likely a rename;
  flag it to the user rather than silently merging.
- `ApiChange` is capped at 5000 rows per run. Runs that wholesale rewrote
  a project log a warning; fall back to comparing two graph snapshots.

---

## Full exploration loop (pseudocode the AI should follow)

```
1. IF project is Fantom-language:
     hits  = searchFantomCode(keyword, projectId)
   ELSE:
     hits  = semanticCodeSearch(natural_language_query, projectId)

2. pick the best hit → anchor_qn

3. callers = getCallers(anchor_qn, projectId, maxDepth=1)
   IF callers.length == 0:
     anchor might be a leaf — look one level wider or pick another symbol

4. callees = getCallees(anchor_qn, projectId, maxDepth=2)
   → direct dependency surface

5. IF about to refactor anchor_qn:
     impact = getCodeImpact(anchor_qn, projectId, maxDepth=3)
     → full blast radius, including inheritance

6. OPTIONAL: similar = findSimilarCode(anchor_qn, projectId)
   → surface duplicates / patterns that may need the same change

7. IF Fantom: detail = getFantomFunction(anchor_qn)
   ELSE:     open the filePath from step 3's result to read source

8. IF editing:
     Edit / Write the source files
     reindexChangedFiles(paths: [...all files touched])  # NON-OPTIONAL
     searchFantomCode(new_symbol)                        # verify discoverability
     getCallers(new_qn)                                  # verify call sites
```

Treat step 8's `reindexChangedFiles` as mandatory after edits. Skipping
it leaves the index lying about the codebase, and the verification queries
will return stale data.

---

## Verification script

A live verifier ships at `scripts/verify-graph-tools.mts`. It calls the
underlying LadybugDB query layer directly (the same code path every MCP
graph tool uses), so it doesn't need an OAuth token. Run after any change
to `src/graph/*` to catch regressions.

```bash
./scripts/stop-server.sh                              # release Ladybug file lock
./node_modules/.bin/tsx scripts/verify-graph-tools.mts
./scripts/start-server.sh                             # bring the server back
```

Expected output (numbers will drift as projects get re-indexed):

```
Project 267 graph: 102575 nodes, 29070 edges

1/10 listFantomProjects OK (240 projects)
2/10 getFantomCodeStats indexer reachable
3/10 searchFantomCode index reachable
4/10 semanticCodeSearch vector store reachable
5/10 findSimilarCode shares vector store
6/10 getCallers OK (219 at depth 1)
7/10 getCallees OK (7 at depth 2)
8/10 getCodeImpact OK (916 affected)
9/10 getFantomFunction OK (resolved + signature present)
10/10 getFantomType resolved

ALL 10 GRAPH TOOLS OK
```

Fixtures used:
- Hub: `SandStar Website::Str` (219 direct callers)
- Leaf: `SandStar Website::OAuthRegisterController.__invoke` (entry point)
- Fantom sample: `bassgPointHealth::Build.publish`
- Type sample: `sys::Str`

---

## Known limitations

- **`getFantomCodeStats` / `getFantomFunction` / `getFantomType` /
  `searchFantomCode` return empty for non-Fantom projects** (they read the
  in-memory `FantomCodeIndexer`, which only loads `.fan` files). Use the
  graph tools for PHP, TypeScript, Vue, etc.
- **`getCodeImpact` with `maxDepth > 5` can be slow** on very wide projects
  — SandStar's `Str` at depth 3 traverses tens of thousands of edges. Start
  at 2, widen only with a specific question.
- **MCP responses are truncated past ~70k characters.** For deep walks,
  call with a small `maxDepth` / `limit` and widen iteratively. For
  `listFantomProjects` in particular, filter with `compatibleWith` or use
  the admin `/admin/graph/ladybug-stats` endpoint instead.

---

## Why this works end-to-end

The code graph is stored as a Kuzu property graph at
`.cache/fantom-graph.db`. Every `*.fan`, `*.ts`, `*.vue`, `*.php`, etc.
file is parsed (regex + tree-sitter) into `CodeNode`s and `CodeEdge`s with
edge types `calls, contains, extends, implements, overrides, returns,
parameters, uses`. The MCP tools are thin wrappers over
`src/graph/ladybugQueryManager.ts`, which issues native Cypher
(`MATCH (a)-[e:CodeEdge*1..N]->(b) WHERE ALL(r IN rels(e) …)`). That means
the traversals are O(graph traversal) rather than O(full-graph scan) — a
depth-5 caller lookup on a 447k-edge graph returns in tens of milliseconds.

See `~/.claude/plans/lovely-baking-church.md` for the full migration
history and `src/graph/ladybugSchema.ts` for the on-disk schema.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.