Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ about one change had to read 95 of 43,793 methods, and every true direct caller
| `axiomcode test-impact` | which tests have to run for the current edit, with the chain that reaches each |
| `axiomcode changed` | which declarations an edit changed, and how (signature, type, body, added, removed). `--impact` adds what that reaches |
| `axiomcode context "<task>"` | where a task's words land in the code, when you have a problem statement and not yet a name |
| `axiomcode graph` | the whole graph as one self-contained HTML page, at `.axiomcode/graph/graph.html` |
| `axiomcode graph` | the whole graph as one self-contained HTML page, at `.axiomcode/graph/graph.html`, drawn from the existing graph (rebuilt first only when stale, with the flags it was indexed with) |
| `axiomcode index` | build or rebuild the graph explicitly; `--lang`, `--src` and `--library` narrow it |
| `axiomcode mcp` | serve the graph to an agent as MCP tools over stdio |

Expand Down
2 changes: 1 addition & 1 deletion plugins/axiomcode/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def axiomcode_test_impact(repo: str = ".", range: str = '', staged: bool = False

@srv.tool()
def axiomcode_graph(repo: str = ".", out: str = '') -> str:
"""Draw the graph as one interactive HTML page (<repo>/.axiomcode/graph/graph.html, or out=<folder|page.html>); runs index first if there is no graph yet."""
"""Draw the graph as one interactive HTML page, for a person: every language the repository was indexed in, at <repo>/.axiomcode/graph/graph.html or out=<folder|page.html>. Drawn from the existing graph when it is up to date (seconds, no engine run); a graph that is out of date is rebuilt first with the --lang, --src and --library it was indexed with, never for a language the index left out; with no graph yet the repository is indexed first. Answers with what it drew, in prose, and the page's absolute path."""
return run(['graph', repo] + (['--out', out] if out else []))

if __name__ == '__main__':
Expand Down
5 changes: 3 additions & 2 deletions plugins/axiomcode/skills/axiomcode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Read / Grep results as `graph: …` lines.
| "how does A reach B" · "everything that reaches X" | `axiomcode path A B` · `axiomcode path '*' X` |
| "what did my edit touch" · "which tests do I run" | `axiomcode changed --impact` · `axiomcode test-impact` |
| "is it safe to delete X" | `axiomcode impact X --delete` |
| the graph as a page for a human · this repo should prefer the graph, once | `axiomcode graph` · `axiomcode install` |
| the graph as a page for a human · this repo should prefer the graph, once | `axiomcode graph` (drawn from the existing graph in seconds; a stale one is rebuilt first with the flags it was indexed with; prints the page's absolute path) · `axiomcode install` |

Rules that decide whether an answer means anything:

Expand All @@ -38,7 +38,8 @@ Rules that decide whether an answer means anything:
briefly on its own only when the answer touches an edited file and the rebuild is nearly done.
- **Before a delete or a rename, ask with `--fresh`** (MCP `fresh=true`): it waits for the rebuild, printing its
progress, and answers from a graph that includes every edit.
A manual `index` with different flags (e.g. dropping `--library`) rebuilds a worse graph over the good one.
A manual `index` with different flags rebuilds a worse graph over the good one. A bare `index`, the background
refresh and `graph` keep the `--lang` (and `--src`, `--library`) the graph was indexed with; pass `--lang` to change it.
- A repo in several languages is indexed in all of them, one graph each, and every query asks each graph; calls
are not followed from one language to another. `--lang` restricts it, `--src src` narrows it; `--library <roots>` so calls into dependencies
resolve (without it they are `ambiguous_unknown` — do not quote that resolution rate).
Expand Down
29 changes: 21 additions & 8 deletions plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
ax_fresh.py worker <repo> the worker itself (what kick detaches)
ax_fresh.py lock <fd> take the build lock on an fd the calling shell holds open
ax_fresh.py count <dir> the source files of each language, walked as the parser walks
ax_fresh.py chosen <repo> the --lang and --src an explicit index chose, which a rebuild keeps

Environment: AXIOMCODE_NO_REFRESH=1 turns every trigger off; AXIOMCODE_REFRESH_DEBOUNCE (seconds, default 2)
is the quiet window; AXIOMCODE_FRESH_WAIT (seconds, default 30) is the most a query whose answer touches an edited file
Expand Down Expand Up @@ -309,6 +310,18 @@ def legacy_params(repo):
src = os.path.relpath(os.path.realpath(run.get('source_dir') or repo), os.path.realpath(repo))
return dict(lang=run.get('language', ''), src_arg='' if src == '.' or src.startswith('..') else src, library=run.get('library_roots') or '')

def rebuild_env(t, **extra):
"""the environment that rebuilds the graph a file table `t` describes, with the language, --src and --library it
was built with: what the background refresh runs, and what `axiomcode graph` runs when the graph is stale. Languages
DETECTED are detected again (a language the repository gained since gets its graph); only a --lang the user gave is
kept. A table from before this was recorded counts as detected: it was built with one language even where the
repository had several, and keeping that would keep the others out for good."""
env = dict(os.environ, AXIOMCODE_LANG='' if t.get('lang_auto', True) else t.get('lang', ''), AXIOMCODE_SRC=t.get('src_arg', ''), **extra)
env.pop('AXIOMCODE_LIBRARY', None)
env.pop('AXIOMCODE_LANG_AUTO', None); env.pop('AXIOMCODE_GRAPH_LANG', None)
if t.get('library'): env['AXIOMCODE_LIBRARY'] = t['library']
return env

def _flock(fd, block):
"""an advisory lock on fd; the OS releases it when the last holder of the file description exits, so a
killed build never leaves a lock behind (no stale-lock timeout to guess)"""
Expand Down Expand Up @@ -447,14 +460,7 @@ def worker(repo):
if st.get('failed_table') == fp: return 0
n = sum(len(x) for x in c)
why = (f"{n} file(s) changed" if n else f"HEAD moved to {(head(repo) or '')[:10]}") + f", found by {os.environ.get('AXIOMCODE_REFRESH_TRIGGER') or 'an edit'}"
# languages DETECTED are detected again (a language the repository gained since gets its graph); only a
# --lang the user gave is kept. A table from before this was recorded counts as detected: it was built with
# one language even where the repository had several, and keeping that would keep the others out for good
env = dict(os.environ, AXIOMCODE_LANG='' if t.get('lang_auto', True) else t.get('lang', ''), AXIOMCODE_SRC=t.get('src_arg', ''),
AXIOMCODE_BACKGROUND='1', AXIOMCODE_REFRESH_REASON=why)
env.pop('AXIOMCODE_LIBRARY', None)
env.pop('AXIOMCODE_LANG_AUTO', None); env.pop('AXIOMCODE_GRAPH_LANG', None)
if t.get('library'): env['AXIOMCODE_LIBRARY'] = t['library']
env = rebuild_env(t, AXIOMCODE_BACKGROUND='1', AXIOMCODE_REFRESH_REASON=why)
t0 = time.time(); write_state(repo, state='building', started=t0, files=sum(len(x) for x in c))
if any(c): print(f"{time.strftime('%H:%M:%S')} refresh: {sum(len(x) for x in c)} file(s) changed ({', '.join((c[0] + c[1] + c[2])[:5])}) — rebuilding", flush=True)
else: print(f"{time.strftime('%H:%M:%S')} refresh: HEAD moved — moving the baseline to it", flush=True)
Expand Down Expand Up @@ -826,6 +832,13 @@ def main(argv):
subdirs[:] = [x for x in subdirs if not prunes('python', x, os.path.basename(d))]
n += sum(1 for f in files if python_script(os.path.join(d, f), f))
print(n); return 0
if cmd == 'chosen':
# the --lang (and --src) the graph here was indexed with, as "<langs>\t<src>", when they were CHOSEN (an explicit
# --lang); nothing when its languages were detected, or there is no graph. axiomcode-build keeps them for a rebuild
# that names no language, so no rebuild path solves a language the index left out
t = load_table(repo)
if has_graph(repo) and t and t.get('lang_auto') is False and t.get('lang'): print(f"{t['lang']}\t{t.get('src_arg', '')}")
return 0
if cmd == 'uptodate':
# exit 0 when the recorded table was built with this language / --src / --library and no file differs
lang, src_arg, lib = argv[3], argv[4] if len(argv) > 4 else '', argv[5] if len(argv) > 5 else ''
Expand Down
16 changes: 9 additions & 7 deletions plugins/axiomcode/skills/axiomcode/scripts/axiomcode
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
# --in is repeatable and takes a list, and several are COMBINED, not intersected, so a change that
# spans two roots is answerable in one call.
# Ends by saying what it could not see. Every other verb needs a name you already have; this one does not.
# axiomcode graph [<repo>] [--out <folder | page.html>] [same flags]
# the graph as one page. Runs the pipeline only when the repo has no up-to-date graph (~1 s otherwise). <repo>
# defaults to the current directory; the page goes to <repo>/.axiomcode/graph/graph.html, or to --out: a folder
# gets <folder>/<repo>.html, a .html path is used as given. The page embeds the sources, so a node opens its code.
# axiomcode graph [<repo>] [--out <folder | page.html>]
# the graph as one page, every language the repository was indexed in. Drawn from the existing graph when it is up
# to date (seconds, no engine run); rebuilt first only when a source file changed, and then with the --lang, --src
# and --library it was indexed with, never for a language the index left out; indexed first when there is none.
# Prints what it drew and the page's absolute path: <repo>/.axiomcode/graph/graph.html, or --out (a folder gets
# <folder>/<repo>.html, a .html path is used as given). The page embeds the sources, so a node opens its code.
# axiomcode path <from> <to> [<repo>] [--every | --paths N] [--all] [--limit N] [--in <path fragment>] [--json] [--fresh]
# the shortest chain of calls from A to B per reached target, and with --every all the routes; through what?
# '*' as one endpoint: path '*' X = everything that can reach X, with the entry points among them; path X '*' = everything X reaches. Endpoints exactly as written in the code: Owner.method,
Expand Down Expand Up @@ -115,8 +117,8 @@ case "${ARGS[0]:-}" in -h|--help) if [ -n "$cmd" ] && [ "${#ARGS[@]}" = 1 ]; the
# check and an exec. The repository is the last argument that is a directory, as the verbs themselves take it, else the
# working directory. No graph: nothing here, the verb builds its first one.
# `changed` and `test-impact` ask about an edit, against the graph of the baseline (kept aside by a refresh), so they do
# not wait for the graph; they wait only when HEAD moved and the baseline has to follow it. `graph` builds (axiomcode-build)
# before it draws, so it is current by construction.
# not wait for the graph; they wait only when HEAD moved and the baseline has to follow it. `graph` checks the graph
# itself: it draws from it when it is current, and rebuilds it with its own recorded flags only when it is not.
# A flag's VALUE is never the repository: `--in plugins/core` names a directory under it, and taking that for the
# repository found no .axiomcode/lang there, so a repository in several languages was asked in its main graph alone and
# a scope that only another language's graph holds was refused as absent.
Expand All @@ -143,7 +145,7 @@ case "$cmd" in
impact) exec python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-impact" ${ARGS[@]+"${ARGS[@]}"} ;;
changed) exec python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-changed" ${ARGS[@]+"${ARGS[@]}"} ;;
test-impact|tests) exec python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-test-impact" ${ARGS[@]+"${ARGS[@]}"} ;;
graph) exec python3 "$H/axiomcode-graph" build ${ARGS[@]+"${ARGS[@]}"} ;;
graph) exec python3 "$H/axiomcode-graph" ${ARGS[@]+"${ARGS[@]}"} ;;
install) exec python3 "$H/axiomcode-install" ${ARGS[@]+"${ARGS[@]}"} ;;
--verbs) verbs ;; # what this dispatcher answers to; bin/axiomcode asks, so the two cannot drift
""|-h|--help|help) if [ ${#ARGS[@]} -gt 0 ]; then verbhelp "${ARGS[0]}"; else helptext; fi ;;
Expand Down
22 changes: 20 additions & 2 deletions plugins/axiomcode/skills/axiomcode/scripts/axiomcode-build
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
# AXIOMCODE_ENGINE checkout of axiomcodegraph (default: the one this skill lives in)
# AXIOMCODE_SRC subtree to analyse (e.g. one module of a monorepo); the graph still lives at <repo>/.axiomcode
# AXIOMCODE_LANG restrict to java | typescript | python | javascript | csharp, or a comma list of them, the first being
# the main one (default: every language with files; the main one is the language with the most)
# the main one (default: every language with files; the main one is the language with the most).
# A rebuild that names no language keeps the ones an earlier --lang chose (and its --src); pass
# --lang again, or AXIOMCODE_REINDEX=1, to choose again
# AXIOMCODE_LIBRARY comma-separated library roots (source trees or IR) passed as --library; without them, calls into
# dependencies are declared unknown rather than guessed
# A repository in several languages gets one graph per language. The main language's is the one it always had,
Expand All @@ -15,7 +17,23 @@
# language's compile; until the last one is in, .axiomcode/out/building names the languages still to come.
set -euo pipefail
H="$(cd "$(dirname "$0")" && pwd)"
REPO="$(cd "${1:-.}" && pwd)"; SRC="$REPO/${AXIOMCODE_SRC:-}"; SRC="${SRC%/}"
REPO="$(cd "${1:-.}" && pwd)"
# A REBUILD THAT NAMES NO LANGUAGE KEEPS THE LANGUAGES THE INDEX CHOSE. Every rebuild comes here: an explicit `index`,
# the background refresh after an edit or a commit, a query that repairs a broken graph, `graph`. One that named no
# language detected every language in the tree, so on a repository indexed with --lang csharp it solved a few stray
# .ts and .js files too (a JavaScript Souffle compile of up to an hour), left a graph for each that later queries
# answered from, and recorded the detected list as the index's choice, so every refresh after it did the same. When
# the graph here was built with an explicit --lang (lang_auto false in its file table), a request with no language
# keeps that --lang, and the --src with it when none is given; a graph whose languages were detected is detected again.
if [ -z "${AXIOMCODE_LANG:-}" ] && [ -z "${AXIOMCODE_REINDEX:-}" ]; then
KEPT="$(python3 "$H/ax_fresh.py" chosen "$REPO" 2>/dev/null || true)"
if [ -n "$KEPT" ]; then
AXIOMCODE_LANG="${KEPT%%$'\t'*}"; export AXIOMCODE_LANG
[ -n "${AXIOMCODE_SRC:-}" ] || { AXIOMCODE_SRC="${KEPT#*$'\t'}"; export AXIOMCODE_SRC; }
[ -n "${AXIOMCODE_BACKGROUND:-}" ] || echo "keeping the languages this graph was indexed with (--lang $AXIOMCODE_LANG${AXIOMCODE_SRC:+ --src $AXIOMCODE_SRC}); pass --lang, or AXIOMCODE_REINDEX=1, to choose again"
fi
fi
SRC="$REPO/${AXIOMCODE_SRC:-}"; SRC="${SRC%/}"
# THE ENGINE IS CHOSEN THE SAME WAY EVERY TIME, AND A BUILD THAT FINDS NONE SAYS WHERE IT LOOKED. The background
# refresh runs this script from a hook, with the hook's PATH, the hook's Node and, installed from a marketplace, from a
# plugin COPIED outside any checkout; an explicit `axiomcode index` runs it from inside the npm package. Each found an
Expand Down
Loading
Loading