Skip to content

Commit c1b60dd

Browse files
mcp, cli: a list of sites comes one per line, path:line: code [tag]
impact, path, context and test-impact take --grep (--grep-limit N): the verb runs with --json and ax_grep.py prints each site the way grep does, with the graph's certainty as a trailing tag, capped with the rest counted by kind. Tests fold to one row per test file (+N more in this file), so one file of many tests no longer spends the whole cap. The MCP tools answer this way by default; full=True, and parameters only the prose carries (source, why, delete, page), give the verb's own answer unchanged. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
1 parent aaf4267 commit c1b60dd

18 files changed

Lines changed: 519 additions & 27 deletions

File tree

‎plugins/axiomcode/mcp/server.py‎

Lines changed: 27 additions & 14 deletions
Large diffs are not rendered by default.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ Read / Grep results as `graph: …` lines.
1313

1414
**Trust the answer, and know what it is.** A `[resolved]` / `[sound]` row has already been looked up again in the graph (the `verified:` line): do not re-derive it by grepping. Each answer ends with `next:` — the one step to take. For a CHANGE (who calls it, what breaks, which tests), read only the lines you will cite or change. To EXPLAIN how something works, the graph gives the reading order, not the explanation: read each step's body, and continue through every `⚠` (a call the graph lost). `[by name]` / `[text]` rows are leads, not facts.
1515

16+
**A list of sites comes the way grep prints it.** The MCP `impact`, `path`, `test_impact` and `context` (without
17+
`source` / `explain` / `from_`) answer one site per line: `path:line: <the code on that line> [resolved · hop 2 · test …]`,
18+
surest first, capped with a count of the rest; `limit=N` lists more, `full=True` gives the sectioned answer with `next:`.
19+
From the shell the same shape is `--grep` (`--grep-limit N`); without it the answer is the prose.
20+
1621
## Start here
1722

1823
| the question in front of you | the call |

‎plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,7 @@ def mark_answer(out, stale, as_json):
673673
def query_names(verb, args, repo):
674674
"""the names a query asks about (impact's targets, path's endpoints, context's --from), as written"""
675675
names, i, pos = [], 0, []
676+
if '--' in args: args = args[args.index('--') + 1:] # --grep runs the verb under ax_grep.py <verb> <repo> … --: not names
676677
while i < len(args):
677678
a = args[i]
678679
if a in _FLAG_VALUE:

‎plugins/axiomcode/skills/axiomcode/scripts/ax_grep.py‎

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

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

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44
# axiomcode index [<repo>] [--lang java|typescript|python|javascript|csharp] [--src <subdir>] [--library <root>[,<root>…]]
55
# the pipeline: parser → engine → .axiomcode/out/graph.sqlite (+ index). <repo> defaults to the current directory.
6-
# axiomcode context "<the task, in your own words>" [<repo>] [--in <path>[,<path>…]]… [--budget N] [--source] [--json] [--fresh]
6+
# axiomcode context "<the task, in your own words>" [<repo>] [--in <path>[,<path>…]]… [--budget N] [--source] [--json] [--fresh] [--grep]
77
# THE FRONT DOOR: the files and callables a task touches, from the problem statement — when there is no name
88
# to ask about yet. Deterministic: task terms scored against the graph's vocabulary by inverse document
99
# frequency, tests demoted, the closure walked from the best seed per term and ranked by nearest hop.
@@ -16,15 +16,15 @@
1616
# and --library it was indexed with, never for a language the index left out; indexed first when there is none.
1717
# Prints what it drew and the page's absolute path: <repo>/.axiomcode/graph/graph.html, or --out (a folder gets
1818
# <folder>/<repo>.html, a .html path is used as given). The page embeds the sources, so a node opens its code.
19-
# axiomcode path <from> <to> [<repo>] [--every | --paths N] [--all] [--limit N] [--in <path fragment>] [--json] [--fresh]
19+
# axiomcode path <from> <to> [<repo>] [--every | --paths N] [--all] [--limit N] [--in <path fragment>] [--json] [--fresh] [--grep]
2020
# the shortest chain of calls from A to B per reached target, and with --every all the routes; through what?
2121
# '*' 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,
2222
# method (free function or any owner), Type (all its methods), file.ts:123, file.py. Datalog over the graph; every
2323
# printed hop re-checked against graph.sqlite. `axiomcode path --selftest <lang>` replays the engine's expected edges.
2424
# Every hop carries the LINE THE CALL IS WRITTEN ON, how certain the edge is, and what kind of call it is —
2525
# an invocation, a construction, a constructor chain, a super call, a decorator, a property access, a method
2626
# reference — in one vocabulary across all five languages. A hop that is not a call is marked and not counted.
27-
# axiomcode impact <target> [<target>…] [<repo>] [--tests] [--depth N] [--in <path>] [--limit N] [--json] [--kind k] [--fresh]
27+
# axiomcode impact <target> [<target>…] [<repo>] [--tests] [--depth N] [--in <path>] [--limit N] [--json] [--kind k] [--fresh] [--grep]
2828
# what has to be looked at again when a declaration changes: a method, a field / constant / enum member, a type, a
2929
# parameter (Owner.m(p)), a type parameter (Type<T>), a local (Owner.m:v), Type.<init> / Type.<clinit>. Prints what
3030
# must change with it (overrides, subtypes), what directly touches it with WHY and how sure ([resolved] / [in scope] /
@@ -36,7 +36,7 @@
3636
# name, initializer; type header; body only; removed; added) — the working tree against the commit the graph was built
3737
# from, or a branch's commits (--range a..b reads from `git merge-base a b`) — each with the target `impact` takes;
3838
# --impact runs impact on all of them as one change set. Without git, name the files.
39-
# axiomcode test-impact [<repo>] [<file>…] [--range <a>..<b> | --staged] [--in <path>] [--limit N] [--json] [--why]
39+
# axiomcode test-impact [<repo>] [<file>…] [--range <a>..<b> | --staged] [--in <path>] [--limit N] [--json] [--why] [--grep]
4040
# which tests actually have to run for this edit: the test files that reach any changed declaration, with the
4141
# chain, so a selection can be checked rather than trusted. Conservative by design — a test reached only
4242
# through an edge the graph does not encode (reflection, a service loader) will NOT appear.
@@ -50,6 +50,9 @@
5050
# language's, the one with the most files, first). --lang restricts it to one language or a comma list, the first the main one;
5151
# --src limits the analysed tree (e.g. src); --library names dependency roots. --fresh (context, path, impact) waits for
5252
# a refresh in flight and answers from the new graph; without it an answer from a graph older than an edit marks its rows.
53+
# --grep (context, path, impact, test-impact) prints the answer's sites one per line, as grep does: `path:line: <the code
54+
# on that line> [resolved | one of a set | by name | text | hop N | test]`, the first 30 (--grep-limit N) and a count of
55+
# the rest, then whether it was verified and its bound. The MCP tools answer this way by default; full=True is the prose.
5356
case "$0" in */*) H="${0%/*}";; *) H=.;; esac; H="$(cd "${H:-/}" && pwd)" # no `dirname`: see bin/axiomcode
5457

5558
# PYTHON UNDER ANOTHER NAME (#1331). Every verb runs `python3`, which a python.org install on Windows does not
@@ -101,6 +104,8 @@ while [ $# -gt 0 ]; do
101104
--src) export AXIOMCODE_SRC="$2"; shift 2 ;;
102105
--library) export AXIOMCODE_LIBRARY="$2"; shift 2 ;;
103106
--fresh) export AXIOMCODE_FRESH=1; shift ;;
107+
--grep) GREP=1; shift ;;
108+
--grep-limit) GREP=1; GREP_LIMIT="$2"; shift 2 ;;
104109
*) ARGS+=("$1"); shift ;;
105110
esac
106111
done
@@ -128,24 +133,29 @@ FR=.; skip=""; for a in ${ARGS[@]+"${ARGS[@]}"}; do
128133
case "$a" in --in|--from|--budget|--seeds|--depth|--limit|--tests-in|--kind|--range|--old|--new|--file|--page|--page-budget|--out) skip=1; continue ;; esac
129134
[ -d "$a" ] && FR="$a"
130135
done
136+
# --grep (context, path, impact, test-impact): the answer's sites one per line, `path:line: code [tag]`, the way grep
137+
# prints them (ax_grep.py). The verb answers with --json and the sites are printed from that document, so the two
138+
# shapes are one answer. Without it the answer is the verb's own, unchanged.
139+
G=()
140+
case "$cmd" in context|path|impact|test-impact|tests) [ -n "${GREP:-}" ] && G=(python3 "$H/ax_grep.py" "$cmd" "$FR" --limit "${GREP_LIMIT:-30}" --) ;; esac
131141
# A REPOSITORY IN SEVERAL LANGUAGES has one graph per language (.axiomcode/lang/<lang> beside the main one): a query
132142
# asks every one of them (ax_langs.py), so no language's code is left out of an answer. A graph named in
133143
# AXIOMCODE_GRAPH was chosen by the caller and is asked alone.
134144
Q=""; [ -d "$FR/.axiomcode/lang" ] && [ -z "${AXIOMCODE_GRAPH:-}" ] && Q="$H/ax_langs.py"
135145
# (-L too: a pointer whose target is gone is a graph to repair — the refresher does it — not a repository without one)
136146
if [ -f "$FR/.axiomcode/out/graph.sqlite" ] || [ -L "$FR/.axiomcode/out/graph.sqlite" ]; then
137147
case "$cmd" in
138-
context|path|impact) exec python3 "$H/ax_fresh.py" query "$FR" "$cmd" -- python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-$cmd" ${ARGS[@]+"${ARGS[@]}"} ;;
148+
context|path|impact) exec python3 "$H/ax_fresh.py" query "$FR" "$cmd" -- ${G[@]+"${G[@]}"} python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-$cmd" ${ARGS[@]+"${ARGS[@]}"} ;;
139149
changed|test-impact|tests) python3 "$H/ax_fresh.py" baseline "$FR" "${AXIOMCODE_BASELINE_WAIT:-30}" || true ;;
140150
esac
141151
fi
142152
case "$cmd" in
143153
index|build) exec bash "$H/axiomcode-build" ${ARGS[@]+"${ARGS[@]}"} ;;
144-
context) exec python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-context" ${ARGS[@]+"${ARGS[@]}"} ;;
145-
path) exec python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-path" ${ARGS[@]+"${ARGS[@]}"} ;;
146-
impact) exec python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-impact" ${ARGS[@]+"${ARGS[@]}"} ;;
154+
context) exec ${G[@]+"${G[@]}"} python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-context" ${ARGS[@]+"${ARGS[@]}"} ;;
155+
path) exec ${G[@]+"${G[@]}"} python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-path" ${ARGS[@]+"${ARGS[@]}"} ;;
156+
impact) exec ${G[@]+"${G[@]}"} python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-impact" ${ARGS[@]+"${ARGS[@]}"} ;;
147157
changed) exec python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-changed" ${ARGS[@]+"${ARGS[@]}"} ;;
148-
test-impact|tests) exec python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-test-impact" ${ARGS[@]+"${ARGS[@]}"} ;;
158+
test-impact|tests) exec ${G[@]+"${G[@]}"} python3 ${Q:+"$Q" "$FR"} "$H/axiomcode-test-impact" ${ARGS[@]+"${ARGS[@]}"} ;;
149159
graph) exec python3 "$H/axiomcode-graph" ${ARGS[@]+"${ARGS[@]}"} ;;
150160
install) exec python3 "$H/axiomcode-install" ${ARGS[@]+"${ARGS[@]}"} ;;
151161
--verbs) verbs ;; # what this dispatcher answers to; bin/axiomcode asks, so the two cannot drift

‎plugins/axiomcode/skills/axiomcode/scripts/axiomcode-context‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,10 +1129,12 @@ def main(argv):
11291129
if nm not in cur or sc > cur[nm][0]:
11301130
cur[nm] = (sc, len(matched), nm)
11311131
if sy.get('line'): span_of[(f, nm)] = (sy['line'], sy.get('end_line') or sy['line'])
1132+
def decls_of(f):
1133+
return [nm for _sc, _m, nm in sorted(best_in_file.get(f, {}).values(), key=lambda x: (-x[0], -x[1], x[2]))[:DECLS_PER_FILE]]
1134+
# `declared_at` beside the names: the line each one starts on, so a site list (ax_grep.py) can point at it
11321135
RESULT['files'] = [{'file': f, 'how': ('name only, no call path' if d == 99 else f'hop {d}'),
1133-
'symbols': n, 'task_terms_matched': hit,
1134-
'declarations': [nm for _sc, _m, nm in
1135-
sorted(best_in_file.get(f, {}).values(), key=lambda x: (-x[0], -x[1], x[2]))[:DECLS_PER_FILE]]}
1136+
'symbols': n, 'task_terms_matched': hit, 'declarations': decls_of(f),
1137+
'declared_at': [f"{f}:{span_of[(f, nm)][0]}" if (f, nm) in span_of else None for nm in decls_of(f)]}
11361138
for f, (d, hit, n) in shown]
11371139
for f, (d, hit, n) in shown:
11381140
mark = f" [{hit} task term(s)]" if hit else ""

‎skills/axiomcode/SKILL.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ Read / Grep results as `graph: …` lines.
1313

1414
**Trust the answer, and know what it is.** A `[resolved]` / `[sound]` row has already been looked up again in the graph (the `verified:` line): do not re-derive it by grepping. Each answer ends with `next:` — the one step to take. For a CHANGE (who calls it, what breaks, which tests), read only the lines you will cite or change. To EXPLAIN how something works, the graph gives the reading order, not the explanation: read each step's body, and continue through every `⚠` (a call the graph lost). `[by name]` / `[text]` rows are leads, not facts.
1515

16+
**A list of sites comes the way grep prints it.** The MCP `impact`, `path`, `test_impact` and `context` (without
17+
`source` / `explain` / `from_`) answer one site per line: `path:line: <the code on that line> [resolved · hop 2 · test …]`,
18+
surest first, capped with a count of the rest; `limit=N` lists more, `full=True` gives the sectioned answer with `next:`.
19+
From the shell the same shape is `--grep` (`--grep-limit N`); without it the answer is the prose.
20+
1621
## Start here
1722

1823
| the question in front of you | the call |
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{"lang": "python", "src": ".",
2+
"checks": [
3+
{"why": "impact --grep: one site per line, path:line: the code on it, and what the graph knows as a trailing tag, surest first, then what reaches it by hop, then the tests; no section headers",
4+
"run": ["impact", "total", "--grep"],
5+
"want": ["shop/cart.py:5: return total(items) [resolved · checkout]",
6+
"shop/cart.py:8: def receipt(items): [hop 2]",
7+
"tests/test_cart.py:4: def test_receipt(): [test · resolved · hop 3]",
8+
"verified: ✓"],
9+
"avoid": ["reads or uses it (", "change: total", "entry points ("]},
10+
{"why": "--grep-limit caps the sites and counts the rest by kind (this case.json names `total`: those are the [text] rows)",
11+
"run": ["impact", "total", "--grep", "--grep-limit", "1"],
12+
"want": ["shop/cart.py:5: return total(items) [resolved · checkout]", "9 [text], 2 [test file], 1 [hop N]"],
13+
"avoid": ["tests/test_cart.py:4:"]},
14+
{"why": "a name written in non-source files is a lead: at most five such rows, the rest counted, and all after the code sites",
15+
"run": ["impact", "total", "--grep"],
16+
"want": ["tests/test_cart.py:4: def test_receipt(): [test · resolved · hop 3]\ncase.json:"],
17+
"avoid": ["[text · names the method]\nshop/"]},
18+
{"why": "tests come one row per test file, its nearest test first and the rest of that file counted: a file of many tests is what gets run, and listed one by one they spent the whole cap",
19+
"run": ["impact", "total", "--grep"],
20+
"want": ["tests/test_checkout.py:4: def test_empty(): [test · resolved · hop 2 · +2 more in this file]"],
21+
"avoid": ["tests/test_checkout.py:8:", "tests/test_checkout.py:12:"]},
22+
{"why": "control: a test file with one test is its row, with no count",
23+
"run": ["impact", "total", "--grep"],
24+
"want": ["tests/test_cart.py:4: def test_receipt(): [test · resolved · hop 3]\n"],
25+
"avoid": ["test_receipt(): [test · resolved · hop 3 · +"]},
26+
{"why": "test-impact --grep folds the same way (the edit named by file: every test that reaches shop/pricing.py)",
27+
"run": ["test-impact", "{repo}", "shop/pricing.py", "--grep"],
28+
"want": ["tests/test_checkout.py:4: def test_empty(): [test · resolved · hop 2 · +2 more in this file]",
29+
"tests/test_cart.py:4: def test_receipt(): [test · resolved · hop 3]"],
30+
"avoid": ["tests/test_checkout.py:8:"]},
31+
{"why": "control: without --grep the answer is the verb's own, sections and all, unchanged",
32+
"run": ["impact", "total"],
33+
"want": ["reads or uses it (1 callable(s): 1 resolved):", "[resolved] checkout shop/cart.py:5 — calls it"],
34+
"avoid": ["shop/cart.py:5: return total(items)"]},
35+
{"why": "control: --json is still one document, untouched by the grep shape",
36+
"run": ["impact", "total", "--json"],
37+
"want": ["\"direct\""],
38+
"stdout_json": true},
39+
{"why": "path --grep: each hop at the line its call is written on, caller → callee, and the hops looked up again",
40+
"run": ["path", "receipt", "total", "--grep"],
41+
"want": ["shop/cart.py:9: return \"paid \" + str(checkout(items)) [resolved · hop 1/2 receipt → checkout]",
42+
"shop/cart.py:5: return total(items) [resolved · hop 2/2 checkout → total]",
43+
"verified: ✓ (2 edge(s) looked up again)"]},
44+
{"why": "path --grep with no chain: the reason is the answer, said as the verb says it, never a JSON document",
45+
"run": ["path", "unused", "checkout", "--grep"],
46+
"want": ["no chain of resolved calls connects unused and checkout"],
47+
"avoid": ["\"answers\""],
48+
"expect_error": true},
49+
{"why": "context --grep: the declarations the task lands on, one per line",
50+
"run": ["context", "the total of a checkout", "--grep"],
51+
"want": ["shop/pricing.py:1: def total(items): [entry · matches 'total']", "shop/cart.py:4: def checkout(items): [entry"],
52+
"avoid": ["where this task's words land"]},
53+
{"why": "a name that is not there is refused as the verb refuses it",
54+
"run": ["impact", "nosuch", "--grep"],
55+
"want": ["nothing named 'nosuch'"],
56+
"expect_error": true}]}

‎tests/cases/python/sites-one-per-line/shop/__init__.py‎

Whitespace-only changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from shop.pricing import total
2+
3+
4+
def checkout(items):
5+
return total(items)
6+
7+
8+
def receipt(items):
9+
return "paid " + str(checkout(items))

0 commit comments

Comments
 (0)