Code-graph navigation (codemap profile)
The call graph lives as text documents, not a database (profile:
spec/profiles/geml-codemap/geml-codemap-profile.md): one GEML document per container (module / dir /
file), each with ONE meta (module, src, entry, resolution-default),
empty-body code blocks per method, and up to three CSV edge tables —
#calls (out), #called-by (in), #unresolved (blind spots). The build's
verify has checked that every edge reference resolves.
The moves
# 1. resolve a name — where does a symbol live
node -e "console.log(JSON.stringify(require('./.geml-code-graph/_index/name-lookup.json')['hashtableFind'],null,1))"
# → [{"anchor":"c:hashtable.c#hashtableFind(…)","doc":"hashtable.c.geml","id":"hashtableFind"}, …]
# Multiple entries = real ambiguity (e.g. a .c definition and a .h inline) — inspect each.
# 2. container overview — the module's surface, one glance
head -8 .geml-code-graph/hashtable.c.geml # meta: entry = the externally-called methods
# 3. open the method block (src= tells you exactly where the code is)
geml get .geml-code-graph/hashtable.c.geml '#hashtableFind'
# 4. forward: what it calls (grep your method's rows; follow doc.geml#id refs)
geml get .geml-code-graph/hashtable.c.geml '#calls'
# 5. reverse: who calls it (aggregated, with file:line sites)
geml get .geml-code-graph/hashtable.c.geml '#called-by'
A reference is #id (same document) or sibling.geml#id (that document, that
block) — geml get it the same way. index.geml holds the repo-level view:
app entries in its meta, #modules / #module-edges aggregate tables.
Reading the tables
| Line |
Meaning |
#calls row, empty confidence |
resolved at the document's resolution-default, high confidence |
#calls row kind=candidate |
dispatch ambiguity: one of several implementations, right after its main call row. Treat the SET as the answer, never just the first |
#calls row confidence medium/low |
the extractor is less sure — say so when reporting |
#unresolved rows (hidden table) |
calls the extractor could NOT resolve — blind spots, not evidence of absence; fall back to grep when one matters |
#called-by absent for a method |
no resolved callers. Under resolution-default = heuristic that means little; under cpg it is strong (but pointer/dynamic dispatch still lands in #unresolved) |
Symbol classes: .accessor (bean get/set/is leaves — the graph view hides
them by default, tables keep them) · .leaf (calls nothing, only called — usually skippable when
tracing logic) · .test (test territory) · .flow-entry (critical-flow start).
"看下/更新下 X 项目的 code-graph" — the end-to-end move
The toolkit ships inside the @geml/geml package: geml codemap …
(without a global install: npx -y @geml/geml codemap …; inside the geml
repo: node geml-parser/dist/geml.js codemap …).
Dispatch first — generation is slow, the conversation must not block on it
Indexers take real time (scip: seconds–minutes; Joern on a repo: minutes).
Pick the executor BEFORE starting:
- Codemap exists, user wants to look → inline, seconds:
serve --background + open the browser. No subagent.
- Update asked and
_index/refresh.json exists → no subagent either:
geml codemap refresh <dir> --background (detached process, costs the
conversation nothing). Open the CURRENT graph immediately — serve renders
live, so when the refresh lands, F5 shows it; say exactly that.
- geml files must be (re)generated agentically — first build, no recipe
recorded, adapters change, or a refresh failed → hand the WHOLE generation
to ONE subagent (Agent tool;
run_in_background: true so the user can keep
working). Its prompt must be self-contained: project root; detect the
languages per the table below (never ask); the exact indexer +
geml codemap build --history + geml codemap verify commands; verify
MUST exit 0; write _index/refresh.json with the exact commands used;
return container/method/entry counts, verify result, and any language
gaps. The MAIN conversation does the last mile itself when the subagent
reports: serve --background, open the browser (if an older codemap was
already on screen, telling the user to F5 is the whole move).
Have a codemap? <proj>/.geml-code-graph/index.geml exists → skip to
step 4 (view) or step 3 (update was asked). An older codemap//graph/
tree from before the rename is not special: regenerate into
.geml-code-graph/ (one build; carry the *.gemlhistory sidecars over
first if they matter) and remove the old directory.
Detect the language(s) — NEVER ask the user. (Steps 2–3 are the
generation work — per Dispatch above they normally run inside the
subagent.) Judge from manifests
first, then source-file counts (Glob/ls). Multiple languages with
real code (≥ a handful of files each) → one build with REPEATED
--adapter groups; the codemap merges them (Java+TS validated).
| Signal |
Indexer → adapter |
tsconfig.json / mostly .ts .tsx .js |
npx --yes @sourcegraph/scip-typescript index --output index.scip (run IN the target repo/subproject) → --adapter scip --raw index.scip |
React / JSX (.tsx .jsx) |
same scip route, verified tier: <Child /> render edges, custom-hook calls, and useReducer(reducer, …) wiring all resolve high — arrow components (const Foo = () =>) included. Indirect dispatch is absent, not #unresolved: callback-prop calls (onToggle(…)), dispatch()→reducer case handling, and context-injected functions ride scip locals/members and leave NO edge — grep when one matters. Also invisible: memo()/forwardRef()-wrapped components (const = call, inner fn is a local) and module-scope render(<App />) callers |
Cargo.toml / .rs |
rust-analyzer scip . --output rust.scip (run IN the crate/workspace root; missing → rustup component add rust-analyzer or the rust-analyzer GitHub releases page) → --adapter scip --raw rust.scip. Precise tier: rust-analyzer-resolved, cross-file/cross-crate calls included; calls into std/external crates land in #unresolved |
pom.xml / build.gradle / .java |
Joern (locate per Locating Joern below; JDK required): GEML_SRC=<abs-src> GEML_OUT=<abs-raw> GEML_LANG=JAVASRC joern --script <pkg>/codemap/joern-export.sc → --adapter joern --raw <raw>. GEML_LANG takes Joern's --language names, UPPERCASE — lowercase javasrc fails with "No CPG generator exists" |
.c / .h |
same Joern route, GEML_LANG=NEWC (valkey-validated) |
.py / go.mod / .kt |
Joern frontends, GEML_LANG=PYTHONSRC etc. (usable tier — SAY SO in your report) |
only a code-review-graph graph.db |
--db <graph.db> (heuristic tier — say so) |
| none of the above |
report honestly which languages are unsupported; do not guess |
.vue / .svelte SFCs: covered — use the AUTO build (geml codemap build --root <proj>), not the manual per-indexer route. It virtualizes
each SFC project (Volar / svelte2tsx, fetched hermetically via npx) into
shadow TS with line-map sidecars, runs one scip pass over shadows + the
project's real TS/JS, and attributes every symbol back to the original
file and line. Template event handlers surface as edges from a synthetic
<Component>.template node (@click="save" → #App-template, #save;
mustapi-validated across three Vue apps, 85/85 SFCs). Honest residuals —
say them when reporting: component-TAG usage (<Child/>) is not a call
edge; Nuxt auto-imports (unimported ref, auto-registered components)
don't resolve, so those references drop; top-level <script setup>
calls, including computed(() => …) bodies, drop exactly like
module-level calls in plain TS; a failed virtualization falls back to
plain TS indexing and says so.
Vendored source trees explode the job list — next.js's
packages/next/src/compiled/ carries ~140 checked-in package.json bundles,
each becoming its own scip job. Prune them at build time:
geml codemap build --root <proj> --exclude "src/compiled/**" (repeatable;
the exclusion also keeps their symbols out of the graph).
Locating Joern — never hardcode a path. Resolve it fresh on each run,
in this order: (1) joern on PATH — if joern --version works, use it;
(2) else read ~/.claude/skills/geml-code-graph/config.json ({"joern": "<launcher-or-dir>"})
and pass it as geml codemap build … --joern <path> (or export GEML_JOERN);
(3) else ASK the user for the joern-cli location (Windows: the folder unzipped
from joern-cli.zip; macOS/Linux: the joern-install.sh install dir), WRITE it
into that JSON file, then reuse it. <path> may be the launcher itself or the
directory holding it (joern.bat on Windows, joern on unix). Ask at most
once per machine — after that the JSON answers. Mirrors the CLI's own
--joern / GEML_JOERN resolution.
Build + verify (also the "更新" path — builds are deterministic,
only changed documents are rewritten):
geml codemap build --adapter scip --raw index.scip --root <proj> \
--out <proj>/.geml-code-graph --history # --container module|dir|file: match
# the layout (default dir; flat C repo → file)
geml codemap verify <proj>/.geml-code-graph # MUST exit 0 before showing anyone
First successful build: record the recipe so refresh (and the
commit hook) can replay it — write <proj>/.geml-code-graph/_index/refresh.json
with the EXACT commands you ran:
{ "root": "..",
"steps": ["npx --yes @sourcegraph/scip-typescript index --output index.scip",
"geml codemap build --adapter scip --raw index.scip --root . --out .geml-code-graph --history",
"geml codemap verify .geml-code-graph"] }
From then on, "更新下" = geml codemap refresh <proj>/.geml-code-graph (skips
itself when git HEAD hasn't moved; log at _index/refresh.log).
View — finish with the browser OPEN, not with instructions.
geml codemap serve <proj>/.geml-code-graph --background # detached: SURVIVES the agent session;
# http://localhost:8140, pages render live
# from .geml — rebuild + F5, never stale.
# already-running port → reused, not stacked.
geml codemap serve <proj>/.geml-code-graph --stop # stop it (pid: .geml-code-graph/_index/serve.pid)
geml codemap render <proj>/.geml-code-graph # serverless alternative: bake .html next to
# each doc; open file:///…/.geml-code-graph/index.html
Always --background (a viewer must not die with the session). Then open
it for the user: Windows start "" <url> (or Start-Process <url>),
macOS open <url>, Linux xdg-open <url>. Port taken by something
else → pick another (--port), open that one.
index.html is the module overview; clicking a module opens its page inside
the graph area (nested view). Method pages: click = callee chain, ⊕ on an
entry = full caller chain, breadcrumb walks back up.
Keep it in sync on every commit (optional per-project hook)
With the recipe recorded (step 3), a Claude Code PostToolUse hook makes any
git commit Claude runs in that project refresh the codemap in the
BACKGROUND (never blocks the commit; non-commit commands exit instantly;
projects without refresh.json are silently skipped). Add to the project's
.claude/settings.json:
{ "hooks": { "PostToolUse": [ { "matcher": "Bash", "hooks": [
{ "type": "command", "command": "geml codemap refresh .geml-code-graph --hook --commit" }
] } ] } }
(.geml-code-graph = the codemap dir relative to the project root; use an absolute
path if the hook cwd differs.) With --commit, the refreshed documents land
as their own follow-up commit — chore(codemap): refresh for <sha>, codemap
dir only — so the next push carries code + graph together. It is loop-safe
(the follow-up commit changes no source file, so the refresh it triggers
skips) and it stands down when HEAD moved during the refresh or a merge is in
progress. Drop --commit to keep the old behavior: refreshed files stay in
the working tree for you to include in a later commit.
Between commits (editing-time sync), geml codemap serve <dir> --watch
re-runs the recipe after 30s of quiet whenever an indexed source file
changes — pages render live, so a browser reload shows the new graph.
Add --history [-m msg] to build to snapshot changed documents into
.gemlhistory sidecars — then geml history get .geml-code-graph/<doc>.geml shows
the graph's evolution and geml revert .geml-code-graph/<doc>.geml '#method' --rev -1
rolls one method's edges back. Language maturity tiers and the smoke-test
gate: docs/design/specs/geml-codemap/DESIGN-geml-code-graph.md §3.4. An MCP wrapper
exists (geml mcp --root <dir>, which serves the four read-only
geml_codemap_* tools next to the document tools when the root holds a graph);
the CLI path works without it.
1---2name: geml-code-graph3description: Build, view, update, and navigate a project's call graph as GEML codemap documents. Use when asked to see/update/build a project's code graph or codemap (看下/更新下 code-graph), when asked "who calls X" / "what does X call" / to trace a call chain or impact path, or whenever a .geml-code-graph/ directory with index.geml and _index/name-lookup.json exists. Detects the project's languages itself — never asks the user; viewing ends with the browser OPEN on the graph.4---56# Code-graph navigation (codemap profile)78The call graph lives as **text documents, not a database** (profile:9`spec/profiles/geml-codemap/geml-codemap-profile.md`): one GEML document per container (module / dir /10file), each with ONE meta (`module`, `src`, `entry`, `resolution-default`),11empty-body `code` blocks per method, and up to three CSV edge tables —12`#calls` (out), `#called-by` (in), `#unresolved` (blind spots). The build's13`verify` has checked that every edge reference resolves.1415## The moves1617```sh18# 1. resolve a name — where does a symbol live19node -e "console.log(JSON.stringify(require('./.geml-code-graph/_index/name-lookup.json')['hashtableFind'],null,1))"20# → [{"anchor":"c:hashtable.c#hashtableFind(…)","doc":"hashtable.c.geml","id":"hashtableFind"}, …]21# Multiple entries = real ambiguity (e.g. a .c definition and a .h inline) — inspect each.2223# 2. container overview — the module's surface, one glance24head -8 .geml-code-graph/hashtable.c.geml # meta: entry = the externally-called methods2526# 3. open the method block (src= tells you exactly where the code is)27geml get .geml-code-graph/hashtable.c.geml '#hashtableFind'2829# 4. forward: what it calls (grep your method's rows; follow doc.geml#id refs)30geml get .geml-code-graph/hashtable.c.geml '#calls'3132# 5. reverse: who calls it (aggregated, with file:line sites)33geml get .geml-code-graph/hashtable.c.geml '#called-by'34```3536A reference is `#id` (same document) or `sibling.geml#id` (that document, that37block) — `geml get` it the same way. `index.geml` holds the repo-level view:38app entries in its meta, `#modules` / `#module-edges` aggregate tables.3940## Reading the tables4142| Line | Meaning |43|---|---|44| `#calls` row, empty confidence | resolved at the document's `resolution-default`, high confidence |45| `#calls` row `kind=candidate` | dispatch ambiguity: one of several implementations, right after its main `call` row. Treat the SET as the answer, never just the first |46| `#calls` row confidence `medium`/`low` | the extractor is less sure — say so when reporting |47| `#unresolved` rows (hidden table) | calls the extractor could NOT resolve — **blind spots, not evidence of absence**; fall back to grep when one matters |48| `#called-by` absent for a method | no *resolved* callers. Under `resolution-default = heuristic` that means little; under `cpg` it is strong (but pointer/dynamic dispatch still lands in `#unresolved`) |4950Symbol classes: `.accessor` (bean get/set/is leaves — the graph view hides51them by default, tables keep them) · `.leaf` (calls nothing, only called — usually skippable when52tracing logic) · `.test` (test territory) · `.flow-entry` (critical-flow start).5354## "看下/更新下 X 项目的 code-graph" — the end-to-end move5556The toolkit ships inside the `@geml/geml` package: `geml codemap …`57(without a global install: `npx -y @geml/geml codemap …`; inside the geml58repo: `node geml-parser/dist/geml.js codemap …`).5960### Dispatch first — generation is slow, the conversation must not block on it6162Indexers take real time (scip: seconds–minutes; Joern on a repo: minutes).63Pick the executor BEFORE starting:6465- **Codemap exists, user wants to look** → inline, seconds:66 `serve --background` + open the browser. No subagent.67- **Update asked and `_index/refresh.json` exists** → no subagent either:68 `geml codemap refresh <dir> --background` (detached process, costs the69 conversation nothing). Open the CURRENT graph immediately — serve renders70 live, so when the refresh lands, F5 shows it; say exactly that.71- **geml files must be (re)generated agentically** — first build, no recipe72 recorded, adapters change, or a refresh failed → hand the WHOLE generation73 to ONE subagent (Agent tool; `run_in_background: true` so the user can keep74 working). Its prompt must be self-contained: project root; detect the75 languages per the table below (never ask); the exact indexer +76 `geml codemap build --history` + `geml codemap verify` commands; verify77 MUST exit 0; write `_index/refresh.json` with the exact commands used;78 return container/method/entry counts, verify result, and any language79 gaps. The MAIN conversation does the last mile itself when the subagent80 reports: `serve --background`, open the browser (if an older codemap was81 already on screen, telling the user to F5 is the whole move).82831. **Have a codemap?** `<proj>/.geml-code-graph/index.geml` exists → skip to84 step 4 (view) or step 3 (update was asked). An older `codemap/`/`graph/`85 tree from before the rename is not special: regenerate into86 `.geml-code-graph/` (one build; carry the `*.gemlhistory` sidecars over87 first if they matter) and remove the old directory.882. **Detect the language(s) — NEVER ask the user.** (Steps 2–3 are the89 generation work — per Dispatch above they normally run inside the90 subagent.) Judge from manifests91 first, then source-file counts (`Glob`/`ls`). Multiple languages with92 real code (≥ a handful of files each) → one build with REPEATED93 `--adapter` groups; the codemap merges them (Java+TS validated).9495 | Signal | Indexer → adapter |96 |---|---|97 | `tsconfig.json` / mostly `.ts` `.tsx` `.js` | `npx --yes @sourcegraph/scip-typescript index --output index.scip` (run IN the target repo/subproject) → `--adapter scip --raw index.scip` |98 | React / JSX (`.tsx` `.jsx`) | same scip route, verified tier: `<Child />` render edges, custom-hook calls, and `useReducer(reducer, …)` wiring all resolve high — arrow components (`const Foo = () =>`) included. Indirect dispatch is **absent, not `#unresolved`**: callback-prop calls (`onToggle(…)`), `dispatch()`→reducer case handling, and context-injected functions ride scip locals/members and leave NO edge — grep when one matters. Also invisible: `memo()`/`forwardRef()`-wrapped components (const = call, inner fn is a local) and module-scope `render(<App />)` callers |99 | `Cargo.toml` / `.rs` | `rust-analyzer scip . --output rust.scip` (run IN the crate/workspace root; missing → `rustup component add rust-analyzer` or the rust-analyzer GitHub releases page) → `--adapter scip --raw rust.scip`. Precise tier: rust-analyzer-resolved, cross-file/cross-crate calls included; calls into std/external crates land in `#unresolved` |100 | `pom.xml` / `build.gradle` / `.java` | Joern (locate per **Locating Joern** below; JDK required): `GEML_SRC=<abs-src> GEML_OUT=<abs-raw> GEML_LANG=JAVASRC joern --script <pkg>/codemap/joern-export.sc` → `--adapter joern --raw <raw>`. GEML_LANG takes Joern's `--language` names, UPPERCASE — lowercase `javasrc` fails with "No CPG generator exists" |101 | `.c` / `.h` | same Joern route, `GEML_LANG=NEWC` (valkey-validated) |102 | `.py` / `go.mod` / `.kt` | Joern frontends, `GEML_LANG=PYTHONSRC` etc. (usable tier — SAY SO in your report) |103 | only a code-review-graph `graph.db` | `--db <graph.db>` (heuristic tier — say so) |104 | none of the above | report honestly which languages are unsupported; do not guess |105106 `.vue` / `.svelte` SFCs: covered — use the AUTO build (`geml codemap107 build --root <proj>`), not the manual per-indexer route. It virtualizes108 each SFC project (Volar / svelte2tsx, fetched hermetically via npx) into109 shadow TS with line-map sidecars, runs one scip pass over shadows + the110 project's real TS/JS, and attributes every symbol back to the original111 file and line. Template event handlers surface as edges from a synthetic112 `<Component>.template` node (`@click="save"` → `#App-template, #save`;113 mustapi-validated across three Vue apps, 85/85 SFCs). Honest residuals —114 say them when reporting: component-TAG usage (`<Child/>`) is not a call115 edge; Nuxt auto-imports (unimported `ref`, auto-registered components)116 don't resolve, so those references drop; top-level `<script setup>`117 calls, including `computed(() => …)` bodies, drop exactly like118 module-level calls in plain TS; a failed virtualization falls back to119 plain TS indexing and says so.120121 Vendored source trees explode the job list — next.js's122 `packages/next/src/compiled/` carries ~140 checked-in package.json bundles,123 each becoming its own scip job. Prune them at build time:124 `geml codemap build --root <proj> --exclude "src/compiled/**"` (repeatable;125 the exclusion also keeps their symbols out of the graph).126127 **Locating Joern — never hardcode a path.** Resolve it fresh on each run,128 in this order: (1) `joern` on PATH — if `joern --version` works, use it;129 (2) else read `~/.claude/skills/geml-code-graph/config.json` (`{"joern": "<launcher-or-dir>"}`)130 and pass it as `geml codemap build … --joern <path>` (or export `GEML_JOERN`);131 (3) else ASK the user for the joern-cli location (Windows: the folder unzipped132 from joern-cli.zip; macOS/Linux: the joern-install.sh install dir), WRITE it133 into that JSON file, then reuse it. `<path>` may be the launcher itself or the134 directory holding it (`joern.bat` on Windows, `joern` on unix). Ask at most135 once per machine — after that the JSON answers. Mirrors the CLI's own136 `--joern` / `GEML_JOERN` resolution.1373. **Build + verify** (also the "更新" path — builds are deterministic,138 only changed documents are rewritten):139140 ```sh141 geml codemap build --adapter scip --raw index.scip --root <proj> \142 --out <proj>/.geml-code-graph --history # --container module|dir|file: match143 # the layout (default dir; flat C repo → file)144 geml codemap verify <proj>/.geml-code-graph # MUST exit 0 before showing anyone145 ```146147 **First successful build: record the recipe** so `refresh` (and the148 commit hook) can replay it — write `<proj>/.geml-code-graph/_index/refresh.json`149 with the EXACT commands you ran:150151 ```json152 { "root": "..",153 "steps": ["npx --yes @sourcegraph/scip-typescript index --output index.scip",154 "geml codemap build --adapter scip --raw index.scip --root . --out .geml-code-graph --history",155 "geml codemap verify .geml-code-graph"] }156 ```157158 From then on, "更新下" = `geml codemap refresh <proj>/.geml-code-graph` (skips159 itself when git HEAD hasn't moved; log at `_index/refresh.log`).1604. **View — finish with the browser OPEN, not with instructions.**161162 ```sh163 geml codemap serve <proj>/.geml-code-graph --background # detached: SURVIVES the agent session;164 # http://localhost:8140, pages render live165 # from .geml — rebuild + F5, never stale.166 # already-running port → reused, not stacked.167 geml codemap serve <proj>/.geml-code-graph --stop # stop it (pid: .geml-code-graph/_index/serve.pid)168 geml codemap render <proj>/.geml-code-graph # serverless alternative: bake .html next to169 # each doc; open file:///…/.geml-code-graph/index.html170 ```171172 Always `--background` (a viewer must not die with the session). Then open173 it for the user: Windows `start "" <url>` (or `Start-Process <url>`),174 macOS `open <url>`, Linux `xdg-open <url>`. Port taken by something175 else → pick another (`--port`), open that one.176177`index.html` is the module overview; clicking a module opens its page inside178the graph area (nested view). Method pages: click = callee chain, ⊕ on an179entry = full caller chain, breadcrumb walks back up.180181## Keep it in sync on every commit (optional per-project hook)182183With the recipe recorded (step 3), a Claude Code PostToolUse hook makes any184`git commit` Claude runs in that project refresh the codemap in the185BACKGROUND (never blocks the commit; non-commit commands exit instantly;186projects without `refresh.json` are silently skipped). Add to the project's187`.claude/settings.json`:188189```json190{ "hooks": { "PostToolUse": [ { "matcher": "Bash", "hooks": [191 { "type": "command", "command": "geml codemap refresh .geml-code-graph --hook --commit" }192] } ] } }193```194195(`.geml-code-graph` = the codemap dir relative to the project root; use an absolute196path if the hook cwd differs.) With `--commit`, the refreshed documents land197as their own follow-up commit — `chore(codemap): refresh for <sha>`, codemap198dir only — so the next push carries code + graph together. It is loop-safe199(the follow-up commit changes no source file, so the refresh it triggers200skips) and it stands down when HEAD moved during the refresh or a merge is in201progress. Drop `--commit` to keep the old behavior: refreshed files stay in202the working tree for you to include in a later commit.203204Between commits (editing-time sync), `geml codemap serve <dir> --watch`205re-runs the recipe after 30s of quiet whenever an indexed source file206changes — pages render live, so a browser reload shows the new graph.207208Add `--history [-m msg]` to build to snapshot changed documents into209`.gemlhistory` sidecars — then `geml history get .geml-code-graph/<doc>.geml` shows210the graph's evolution and `geml revert .geml-code-graph/<doc>.geml '#method' --rev -1`211rolls one method's edges back. Language maturity tiers and the smoke-test212gate: `docs/design/specs/geml-codemap/DESIGN-geml-code-graph.md` §3.4. An MCP wrapper213exists (`geml mcp --root <dir>`, which serves the four read-only214`geml_codemap_*` tools next to the document tools when the root holds a graph);215the CLI path works without it.