Skip to content

Commit a878050

Browse files
Merge pull request #1938 from AxiomCodeAI/feat/graph-verb
plugin: axiomcode graph draws the codebase as a page again
2 parents 5f281c9 + d0683fc commit a878050

8 files changed

Lines changed: 901 additions & 9 deletions

File tree

‎plugins/axiomcode/skills/axiomcode/SKILL.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,13 @@ With them, a call into a dependency resolves to its declaration and a chain is t
123123
types (`client.post(...).json()`). Library bodies are not walked, so a dependency calling back into the project is
124124
not found this way.
125125

126+
## graph
127+
128+
`axiomcode graph` draws the codebase as one page for the user to look at: directories, files, types and methods, with
129+
the call edges between them and each edge's tier. It prints the page's path (`.axiomcode/graph/graph.html`); open it
130+
in a browser. It is drawn from the graph the index built, rebuilt first only when a source file changed. It is not an
131+
MCP tool: it answers no question, so when the user asks to see the graph, run it and give them the path.
132+
126133
## What it cannot see — say so instead of guessing
127134

128135
Reflection, string dispatch, event buses; receivers the engine could not type; callbacks invoked by a library; what a

‎plugins/axiomcode/skills/axiomcode/scripts/axiomcode‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
# labelled [asserted]. Alone, lists every link and whether the graph took it; `link <file:line> -` removes one.
1818
# axiomcode index [<repo>] [--lang <lang>[,<lang>…]] [--src <subdir>] [--library <root>[,<root>…]]
1919
# build the graph (the first query builds it too). <repo> defaults to the current directory.
20+
# axiomcode graph [<repo>]
21+
# the codebase as one interactive page: directories, files, types and methods, and the call edges between them.
22+
# Prints the page's path, <repo>/.axiomcode/graph/graph.html.
2023
#
2124
# Names are written as in the code: Owner.method, function, Type, or file.py:123. `axiomcode help <verb>` for one verb.
2225

@@ -182,6 +185,7 @@ done
182185
LASTPOS=""; [ "${#POS[@]}" -gt 0 ] && LASTPOS="${POS[${#POS[@]}-1]}"
183186
case "$cmd" in
184187
index|build) if [ "${#POS[@]}" -gt 0 ] && [ ! -d "${POS[0]}" ]; then gone "${POS[0]}"; fi ;;
188+
graph) case "${POS[0]:-}" in ""|build|export|draw) ;; *) [ -d "${POS[0]}" ] || gone "${POS[0]}" ;; esac ;;
185189
context) if [ "${#POS[@]}" -gt 1 ] && [ ! -d "${POS[1]}" ]; then gone "${POS[1]}"; fi ;;
186190
path) if [ "${#POS[@]}" -gt 2 ] && [ ! -d "${POS[2]}" ]; then gone "${POS[2]}"; fi ;;
187191
impact) if [ "${#POS[@]}" -gt 1 ] && [ ! -e "$LASTPOS" ] && dirlike "$LASTPOS"; then gone "$LASTPOS"; fi ;;
@@ -234,6 +238,7 @@ case "$cmd" in
234238
changed) exec python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-changed" ${ARGS[@]+"${ARGS[@]}"} ;;
235239
test-impact|tests) exec ${G[@]+"${G[@]}"} python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-test-impact" ${ARGS[@]+"${ARGS[@]}"} ;;
236240
link) exec python3 "$H/axiomcode-link" ${ARGS[@]+"${ARGS[@]}"} ;;
241+
graph) exec python3 "$H/axiomcode-graph" ${ARGS[@]+"${ARGS[@]}"} ;;
237242
--verbs) verbs ;; # the dispatch table, internal verbs included; tests/surfaces.py audits it
238243
""|-h|--help|help) if [ ${#ARGS[@]} -gt 0 ]; then verbhelp "${ARGS[0]}"; else helptext; fi ;;
239244
*) echo "axiomcode: unknown subcommand '$cmd' — the entry point has: $(public_verbs | tr '\n' ' ')" >&2

‎plugins/axiomcode/skills/axiomcode/scripts/axiomcode-graph‎

Lines changed: 308 additions & 0 deletions
Large diffs are not rendered by default.

‎plugins/axiomcode/skills/axiomcode/scripts/viewer.html‎

Lines changed: 344 additions & 0 deletions
Large diffs are not rendered by default.

‎skills/axiomcode/SKILL.md‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
---
22
name: axiomcode
33
description: >-
4-
Use for any why, what or where question about code — how a codebase works, where something lives, who calls it, what a change to it breaks, which tests cover an edit. Also use when resolving an issue or bug report, which names a symptom rather than a file. Examples: "How does X work?", "Where do I change Y?", "What calls this?", "What breaks if I change Z?", "Which tests do I run?", "Fix this issue". Search with grep as usual; ask the graph for what grep cannot know — which declaration a call reaches and the callers that never spell the name. Answers come from a resolved call graph, so they include callers that never spell the name — through an interface, an override, a callback, dependency injection or a config key — and every place comes with the code of the function it sits in. Call the MCP tools directly, no need to load this skill first: impact(name) for who calls it and what a change reaches (with no name: your uncommitted edits), path(start, end) for how A reaches B, tests() for the tests your edits reach. Only when those tools are not in your list, the same from the shell: `axiomcode impact <name>`, `axiomcode path <A> <B>`, `axiomcode tests`. Java, TypeScript, Python, JavaScript, C#.
4+
Use for any why, what or where question about code — how a codebase works, where something lives, who calls it, what a change to it breaks, which tests cover an edit. Also use when resolving an issue or bug report, which names a symptom rather than a file. Examples: "How does X work?", "Where do I change Y?", "What calls this?", "What breaks if I change Z?", "Which tests do I run?", "Fix this issue". Search with grep as usual; ask the graph for what grep cannot know — which declaration a call reaches and the callers that never spell the name. Answers come from a resolved call graph, so they include callers that never spell the name — through an interface, an override, a callback, dependency injection or a config key — and every place comes with the code of the function it sits in. Call the MCP tools directly, no need to load this skill first: impact(name) for who calls it and what a change reaches (with no name: your uncommitted edits), path(start, end) for how A reaches B, tests() for the tests your edits reach, context(task, source=True) for how something works as a step-by-step call flow with each step's code. Only when those tools are not in your list, the same from the shell: `axiomcode impact <name>`, `axiomcode path <A> <B>`, `axiomcode tests`, `axiomcode context "<task>" --source`. Java, TypeScript, Python, JavaScript, C#.
55
---
66

77
# axiomcode
88

99
Search with grep as usual; the graph answers what grep cannot. Use the MCP tools when they are in your list (in Claude Code
10-
`mcp__plugin_axiomcode_axiomcode__impact`, `__path`, `__tests`); otherwise run
10+
`mcp__plugin_axiomcode_axiomcode__impact`, `__path`, `__tests`, `__context`); otherwise run
1111
`<this dir>/../../plugins/axiomcode/skills/axiomcode/scripts/axiomcode <verb>` from the repository root. Same answer either way.
1212

1313
| the question | MCP tool | shell |
1414
|---|---|---|
1515
| where is the code for this task? | your own search (grep), then bring the name here | — |
1616
| who calls X, what does changing it reach, which tests? | `impact(name)` | `axiomcode impact <name>` |
17+
| what is the value of constant X, and who reads it? | `impact(name)` | `axiomcode impact <name>` |
1718
| what do my uncommitted edits reach? | `impact()` | `axiomcode impact` |
1819
| how does A reach B? | `path(start, end)` | `axiomcode path <A> <B>` |
1920
| which tests do my edits need, and how do I run them? | `tests()` | `axiomcode tests` |
2021
| an answer lists an unresolved call I can see the target of | `link(site, target)` | `axiomcode link <file:line> <target>` |
22+
| how does this work, start to finish? | `context(task, source=True)` | `axiomcode context "<task>" --source` |
2123

2224
Names are written as in the code: `Owner.method`, `function`, `Type`, or `file.py:123` for the declaration at that
2325
line. There is no setup step: the first question builds the graph, and it refreshes itself after every edit.
@@ -45,7 +47,8 @@ from an empty answer.
4547

4648
With a name: who calls it, what depends on it further out, and the tests that exercise it. Example:
4749
`impact(name="PriceService.total")`. With no name: the first line is `your edits:` (each declaration you changed and
48-
how), then the same answer for all of them.
50+
how), then the same answer for all of them. A constant answers with its value — `change: const MAX_ITEMS = 5` —
51+
so a limit, a default or a threshold is read off the first line rather than from the file.
4952

5053
## path
5154

@@ -87,10 +90,46 @@ identifier as written in the code.
8790

8891
## index
8992

90-
`axiomcode index` builds the graph explicitly; `--lang`, `--src` and `--library` narrow it. Never re-run it on an
93+
`axiomcode index` builds the graph explicitly; `--lang` and `--src` narrow it. Never re-run it on an
9194
existing graph: the graph rebuilds itself after edits, and an answer given before that finishes says so on a
9295
`graph refresh:` line.
9396

97+
By default the graph is built without the project's dependencies: a call into one is unresolved or named only as an
98+
external boundary. When better coverage is needed, compile the dependencies and enable them with `--library`, given
99+
once on `index` and kept by every rebuild after it:
100+
101+
axiomcode index --library auto # the dependencies the project imports
102+
axiomcode index --library .venv/lib/python3.12/site-packages/requests,/deps/ir/jdk # or name them
103+
104+
`auto` finds what the project depends on, in every language: a Python package its source imports, in its virtual
105+
environment (`.venv`, `venv`, `$VIRTUAL_ENV`); a JavaScript / TypeScript package it imports, under `node_modules`; a
106+
Java dependency `pom.xml` or `build.gradle` declares, through its `-sources.jar` in the Maven repository or Gradle's
107+
cache, or its class jar decompiled when it ships no sources (needs Vineflower: `mvn dependency:get
108+
-Dartifact=org.vineflower:vineflower:1.10.1`); a NuGet package a `.csproj` references, decompiled from its assembly
109+
(needs `dotnet tool install -g ilspycmd`). Whatever it cannot find (a package never restored, a jar without sources)
110+
it names, with the command that fetches it. A named entry, comma-separated with no spaces, is a dependency's source
111+
directory or a library IR (a directory of the parser's CSV tables, as `axiomcode parser <source> <dir> --library`
112+
writes it). Every library is compiled once into `~/.cache/axiomcode/libir/` and reused until its files change; one no
113+
build has used for 30 days is removed. Entries are relative to the repository or absolute; write `~` out, since one
114+
after a comma is not expanded. `AXIOMCODE_LIBRARY` takes the same list.
115+
116+
The choice is kept with the graph. Every rebuild after it stages the libraries again (`auto` discovers them
117+
again, so a new dependency is picked up), and with libraries on, a dependency change alone (a manifest or lockfile
118+
edit, `pip install -U`, `npm install`, `dotnet restore`, a Maven or Gradle fetch of a dependency that was missing)
119+
also marks the graph stale and rebuilds it. A graph built
120+
without `--library` never gains libraries by itself; `AXIOMCODE_REINDEX=1 axiomcode index` turns them off again.
121+
122+
With them, a call into a dependency resolves to its declaration and a chain is typed through its declared return
123+
types (`client.post(...).json()`). Library bodies are not walked, so a dependency calling back into the project is
124+
not found this way.
125+
126+
## graph
127+
128+
`axiomcode graph` draws the codebase as one page for the user to look at: directories, files, types and methods, with
129+
the call edges between them and each edge's tier. It prints the page's path (`.axiomcode/graph/graph.html`); open it
130+
in a browser. It is drawn from the graph the index built, rebuilt first only when a source file changed. It is not an
131+
MCP tool: it answers no question, so when the user asks to see the graph, run it and give them the path.
132+
94133
## What it cannot see — say so instead of guessing
95134

96135
Reflection, string dispatch, event buses; receivers the engine could not type; callbacks invoked by a library; what a

‎tests/front_door.py‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
every place with the code of the function it sits in, in a fenced block. That shape is given at the front doors — the
66
installed command (bin/axiomcode sets AXIOMCODE_FRONT) and the MCP server (AXIOMCODE_SURFACE=mcp) — when no flag is
77
passed. Everything that calls the dispatcher directly (the hooks, the case suite, loops) or passes a flag gets the verb's
8-
own answer, unchanged; a verb outside the surface (find, context, graph, diff, install) is refused by the installed
8+
own answer, unchanged; a verb outside the surface (find, diff, install) is refused by the installed
99
command.
1010
1111
a. bin/axiomcode on a small repository (copied to a temporary directory, committed, indexed): impact <name> and
@@ -98,11 +98,14 @@ def main():
9898
if fails: return 1
9999

100100
# ── a. the installed command, no flags ───────────────────────────────────────────────────────────────────
101-
for verb, args in (('find', ('how is the invoice total computed',)),
102-
('graph', ()), ('diff', ()), ('install', ())):
101+
for verb, args in (('find', ('how is the invoice total computed',)), ('diff', ()), ('install', ())):
103102
rc, out, err = cli(repo, verb, *args)
104103
check(f'{verb}: off the surface, the installed command refuses it and names the supported verbs',
105104
rc != 0 and 'impact' in err and 'path' in err and 'tests' in err, (out + err)[:400])
105+
rc, out, err = cli(repo, 'graph')
106+
page = out.strip().splitlines()[-1].split('page: ', 1)[-1] if out.strip() else ''
107+
check('graph: on the surface, it draws the page and prints its path last', rc == 0 and page.endswith('graph.html')
108+
and os.path.isfile(page), (out + err)[-400:])
106109
rc, out, err = cli(repo, 'context', 'how is the invoice total computed')
107110
check('context: on the surface, the task in words answers with the flow', rc == 0 and 'pricing.py' in out, (out + err)[:400])
108111
rc, out, err = cli(repo, 'impact', 'vat_rate')

0 commit comments

Comments
 (0)