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
7 changes: 6 additions & 1 deletion plugins/axiomcode/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ async def call_tool(self, name, arguments, *a, **k): # the SDK: refused as a
# refresher to look: a rebuild if a file changed or HEAD moved, otherwise only the time of the check is recorded. An
# active session updates that time itself, so the timer mostly fires for one that has gone quiet.
SEEN = set()
# the flags whose next argument is their value, as the dispatcher skips them when it looks for the repository
VALUED = {'--in', '--from', '--budget', '--seeds', '--depth', '--limit', '--tests-in', '--kind', '--range', '--old',
'--new', '--file', '--page', '--page-budget', '--out'}
def _timer(interval):
import time
while True:
Expand All @@ -115,7 +118,9 @@ def run(args, cwd=None, timeout=900):
return (f"axiomcode could not start bash ({BASH}): {e}. On Windows it needs the bash that comes with "
"Git for Windows; install it, or set AXIOMCODE_BASH to its bin\\bash.exe.")
except subprocess.TimeoutExpired:
repo = next((a for a in reversed(args[1:]) if os.path.isdir(a)), cwd or os.getcwd())
# a flag's value (`--in <dir>`) is not the repository
pos = [a for i, a in enumerate(args[1:], 1) if args[i - 1] not in VALUED]
repo = next((a for a in reversed(pos) if os.path.isdir(a)), cwd or os.getcwd())
try:
sys.path.insert(0, os.path.dirname(AX)); import ax_contract, ax_fresh
if ax_fresh.building(os.path.realpath(repo)): return ax_contract.building_note(os.path.realpath(repo))
Expand Down
39 changes: 38 additions & 1 deletion plugins/axiomcode/skills/axiomcode/scripts/ax_langs.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,13 @@ def main(argv):
if all(n[2] == 3 for n in named): # no graph has anything to say: the main one says so
sys.stdout.write(named[0][3]); sys.stderr.write(named[0][4]); return 0
named = [n for n in named if n[2] != 3]
named, others = by_landing(named)
answered = [n for n in named if n[2] == 0]
if not answered:
# a graph that does not hold the --in scope says only that; the graph that holds it has the refusal that
# matters (nothing under it matches, or the name is not there), so a scope in one language is judged by it
held = [n for n in named if not n[3].lstrip().startswith(NOT_HELD)]
if held: named = held

if '--json' in args and answered:
objs = []
Expand All @@ -107,15 +113,46 @@ def main(argv):

show = answered or named
if not answered and len({(n[3], n[4]) for n in named}) == 1: # the same refusal from every graph: once
print(f"══ {', '.join(n[0] for n in named)} graphs ══"); sys.stdout.write(named[0][3]); sys.stderr.write(named[0][4])
print(f"══ {', '.join(n[0] for n in named)} graph{'s' if len(named) > 1 else ''} ══"); sys.stdout.write(named[0][3]); sys.stderr.write(named[0][4])
return named[0][2]
for i, (lang, is_main, rc, out, err) in enumerate(show):
if len(show) > 1 or not is_main:
print(('' if i == 0 else '\n') + f"══ {lang} graph ══" + ('' if is_main else f" (.axiomcode/lang/{lang})"), flush=True)
sys.stdout.write(out); sys.stdout.flush(); sys.stderr.write(err); sys.stderr.flush()
if answered and others:
print(f"\nthe --from name is also declared in the {', '.join(others)} graph(s), where its flow reaches "
"half as many of the task's words or fewer; --lang <language> asks one of them")
return 0 if answered else named[0][2]


LANDING = 'axiomcode-from-landing: '
NOT_HELD = 'no indexed file has ' # ax_contract.require_scope: the --in path is not in this graph at all


def by_landing(named):
"""(the answers, the languages left out) — `context --from <name>` in every graph, the language the task lands in first.

A common name (main, run, init) is declared in every language's graph, and each graph starts a flow there. Headed
main-graph-first, the answer a reader met first was a flow in another language than the one the task was about.
Each graph says how many of the task's words land in the files its flow reaches; the graphs where most land come
first, and a graph where half as many or fewer land is left out and named, when some other graph does have them.
"""
land = {}
for i, n in enumerate(named):
err = n[4].splitlines(keepends=True)
for l in err:
if l.startswith(LANDING):
try: land[i] = int(l[len(LANDING):])
except ValueError: pass
named[i] = (*n[:4], ''.join(l for l in err if not l.startswith(LANDING)))
if not land or not any(land.get(i, 0) > 0 and n[2] == 0 for i, n in enumerate(named)): return named, []
# generic words (run, start) land a little in every language: keep the graphs whose flow reaches more than half
# as many of the task's words as the best one's, as from_starts does between the declarations of one graph
best = max(land.get(i, 0) for i, n in enumerate(named) if n[2] == 0)
keep = sorted((i for i, n in enumerate(named) if 2 * land.get(i, 0) > best or n[2] != 0), key=lambda i: -land.get(i, 0))
return [named[i] for i in keep], [n[0] for i, n in enumerate(named) if i not in keep]


if __name__ == '__main__':
if len(sys.argv) < 3: sys.exit(__doc__)
sys.exit(main(sys.argv[1:]))
9 changes: 8 additions & 1 deletion plugins/axiomcode/skills/axiomcode/scripts/axiomcode
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,14 @@ case "${ARGS[0]:-}" in -h|--help) if [ -n "$cmd" ] && [ "${#ARGS[@]}" = 1 ]; the
# `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.
FR=.; for a in ${ARGS[@]+"${ARGS[@]}"}; do [ -d "$a" ] && FR="$a"; done
# 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.
FR=.; skip=""; for a in ${ARGS[@]+"${ARGS[@]}"}; do
if [ -n "$skip" ]; then skip=""; continue; fi
case "$a" in --in|--from|--budget|--seeds|--depth|--limit|--tests-in|--kind|--range|--old|--new|--file|--page|--page-budget|--out) skip=1; continue ;; esac
[ -d "$a" ] && FR="$a"
done
# A REPOSITORY IN SEVERAL LANGUAGES has one graph per language (.axiomcode/lang/<lang> beside the main one): a query
# asks every one of them (ax_langs.py), so no language's code is left out of an answer. A graph named in
# AXIOMCODE_GRAPH was chosen by the caller and is asked alone.
Expand Down
81 changes: 78 additions & 3 deletions plugins/axiomcode/skills/axiomcode/scripts/axiomcode-context
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,51 @@ def score_symbols(g, terms, use_path=True):
return scored


def landing(g, scored):
"""({file: its best symbol score}, {file: the task terms its symbols match}) — where the task's words land, per file"""
best, words = collections.defaultdict(float), collections.defaultdict(set)
for sid, (sc, matched) in scored.items():
f = (g.sym.get(sid) or {}).get('file')
if not f: continue
best[f] = max(best[f], sc); words[f].update(matched)
return best, words


def flow_words(g, ids, landed, depth=4):
"""{id: the task terms that land in a file its flow reaches} — resolved calls only, to the depth a flow is shown"""
_best, words = landed
out = collections.defaultdict(set)
for r in g.q("SELECT DISTINCT caller_id c, callee_method_id m FROM call_edges WHERE callee_method_id IS NOT NULL"):
if r['m'] != r['c']: out[r['c']].add(r['m'])
res = {}
for x in ids:
seen, front = {x}, [x]
for _d in range(depth):
front = [y for f in front for y in out.get(f, ()) if y not in seen and not seen.add(y)]
res[x] = set().union(*(words.get((g.sym.get(y) or {}).get('file'), ()) for y in seen))
return res


def from_starts(g, ids, landed, scopes=()):
"""The declarations a `--from <name>` starts the flow at, of the `ids` that name resolves to.

A common name (main, run, init, handler) is declared in every tool, fixture and test of a repository: starting at
all of them printed a page of unrelated flows, the one the task was about somewhere in it or cut off. A scope the
caller gave is knowledge and keeps only what is under it — none under it is none. Without one, the task's own
words choose: the declarations whose FLOW reaches the files those words land in, most words first. Only the file a
declaration sits in was not enough: `tools/pkg/cli.py main` and `tools/gen/make.py main` both matched "tool" by
their path, while only the first calls into the code the task named. When the words land in no flow, every
declaration stays.
"""
if scopes:
ids = [x for x in ids if any(sc in (g.sym[x].get('file') or '') for sc in scopes)]
if len(ids) < 2: return ids
reach = flow_words(g, ids, landed)
hit = sorted((x for x in ids if reach[x]), key=lambda x: (-len(reach[x]), bool(g.sym[x].get('is_test'))))
# a word as common as the name lands a little everywhere: keep the flows that reach more than half the best's words
return [x for x in hit if 2 * len(reach[x]) > len(reach[hit[0]])] if hit else ids


# How many of a directory's best symbols count toward its score. Swept over the same 18 tasks: 1 scores
# 44% (one lucky symbol decides a package), 2 or 3 score 55%, 5 and 8 both score 61%, 15 drops to 55% and
# 50 to 50% -- by then the score is drifting back toward "how much does this directory contain".
Expand Down Expand Up @@ -838,6 +883,20 @@ def main(argv):
# the terms the report itself marked as code take the budget first (see winnow)
terms = winnow(g, terms, strong=code_terms(body))
scored = score_symbols(g, terms)
landed = landing(g, scored)
# --from names where the flow starts, which is also WHERE to look: a graph that declares it (under the caller's
# --in, when one is given) answers from there even when the task's words land nowhere in it, rather than refusing
# for want of a scope or a matching word. In a repository in several languages every graph is asked (ax_langs), and
# one that does not declare the name refuses, so the answer comes from the language that does.
fixed = [x.strip().replace('\\', '/') for x in scopes] if not scope_offered else []
from_ids = {sel: [x for x in g.resolve(sel)[1] if x in g.sym] for sel in starts}
held = {sel: [x for x in ids if not fixed or any(sc in (g.sym[x].get('file') or '') for sc in fixed)]
for sel, ids in from_ids.items()}
if starts and os.environ.get('AXIOMCODE_FANOUT') and not any(held.values()):
print(f"nothing named {', '.join(repr(s) for s in starts)} in this graph"
+ (f" under {', '.join(fixed)}" if fixed and any(from_ids.values()) else '') + ".")
return 2
have_from = any(held.values())
# --in: a scope the CALLER supplies is knowledge — a stack frame, the file it just read, the package
# named in the issue's labels. That is worth more than any amount of inferring the package from prose,
# so it restricts the seeds outright: everything outside it is dropped BEFORE seeding, and the entry
Expand Down Expand Up @@ -888,7 +947,7 @@ def main(argv):
+ ("…" if n > 4 else "") + ")")
print(" no --in was given, so NOTHING below is narrowed to it — "
"pass --in <path> to narrow.\n")
bad = None if flat else require_scope(g, scopes, terms, rank=rank, flag=' --in-offered')
bad = None if flat or (have_from and not fixed) else require_scope(g, scopes, terms, rank=rank, flag=' --in-offered')
if bad is not None: return bad

# a symbol is kept when it is under ANY of the scopes -- the union, because the caller naming two roots
Expand All @@ -900,7 +959,7 @@ def main(argv):
named = named_declarations(g, body) + route_seeds(g, body)
if scopes and not scope_offered:
named = [(sid, why) for sid, why in named if in_scope(g.sym.get(sid, {}).get('file'))]
if not scored and not named:
if not scored and not named and not have_from:
# the scope is real (require_scope proved it) but holds nothing matching — offer what is under it,
# ranked the same way the scope-less menu is: by where the words land, never by how big a directory is
ranked = rank_dirs(g, scope_terms(g, task))
Expand Down Expand Up @@ -932,7 +991,23 @@ def main(argv):
print(f" {g.disp(sid):46.46} {g.loc(sid):34.34} {why}")

if explain:
roots_ = [i_ for sel in starts for i_ in g.resolve(sel)[1] if i_ in g.sym]
roots_ = []
for sel in starts:
ids = from_ids[sel]
picked = from_starts(g, ids, landed, scopes if not scope_offered else ())
if ids and not picked:
print(f"--from {sel}: none of its {len(ids)} declaration(s) lies under --in; the flow starts at all of them")
picked = from_starts(g, ids, landed)
if len(picked) < len(ids):
print(f"--from {sel}: {len(ids)} declarations by that name; the flow starts at the {len(picked)} "
+ ("under --in" if scopes and not scope_offered else "where the task's words land")
+ f" ({', '.join(g.loc(x) for x in picked[:3])}{'…' if len(picked) > 3 else ''}); --in <path> picks another")
roots_ += picked
if starts and os.environ.get('AXIOMCODE_FANOUT'):
# how many of the task's words land in the files the flow reaches: ax_langs puts the language where they
# land first, since a common --from name (main, run, init) is declared in every language's graph
print(f"axiomcode-from-landing: {max((len(w) for w in flow_words(g, roots_, landed).values()), default=0)}",
file=sys.stderr)
if not roots_:
# no --from: the entry points this verb already chose, the callable ones, first FLOW_ROOTS of them
roots_ = [s_ for s_, _t, _sc in seeds if (g.sym.get(s_) or {}).get('method_id')][:FLOW_ROOTS]
Expand Down
Loading
Loading