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
5 changes: 3 additions & 2 deletions plugins/axiomcode/hooks/changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ def key(d): return f"{d['file']}:{d['symbol']}:{d['kind']}:{d.get('detail', '')}
moved = _graphline.base_moved_line(cwd, st)
new = [d for d in j.get('changed', []) if d.get('target') and key(d) not in seen and not TEST.search(d['file'])]
head_at = ' '.join(x for x in ('HEAD', (j.get('base_moved') or {}).get('new', '')[:10]) if x)
since = f"{head_at}: the commits that came in are not counted" if (AGAINST or j.get('base_moved')) else f"the graph's commit {(j.get('built_at') or '')[:10]}"
since = (f"{head_at}: the commits that came in are not counted" if (AGAINST or j.get('base_moved'))
else "the files the graph was indexed from" if j.get('against_index') else f"the graph's commit {(j.get('built_at') or '')[:10]}")
if new:
lines = summarize(new, f"graph: after that command, {{n}} declaration(s) changed in the working tree (against {since}) —")
st['reported'] = list(seen | {key(d) for d in new})
Expand All @@ -189,7 +190,7 @@ def key(d): return f"{d['file']}:{d['symbol']}:{d['kind']}:{d.get('detail', '')}
new = [d for d in j.get('changed', []) if d.get('target') and key(d) not in seen and not TEST.search(d['file'])]
head_at = ' '.join(x for x in ('HEAD', (j.get('base_moved') or {}).get('new', '')[:10]) if x)
since = (f"against {head_at} (the commits that came in are not counted)" if (AGAINST or j.get('base_moved'))
else f"since the graph's commit {(j.get('built_at') or '')[:10]}")
else "since the graph was indexed" if j.get('against_index') else f"since the graph's commit {(j.get('built_at') or '')[:10]}")
if new:
lines = summarize(new, f"graph: {{n}} declaration(s) changed in the working tree {since} and were not reported yet —")
st['reported'] = list(seen | {key(d) for d in new})
Expand Down
6 changes: 5 additions & 1 deletion plugins/axiomcode/hooks/enrich.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def impact(d):
except Exception: return d, {}
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as ex:
results = list(ex.map(impact, decls[:3])); bodies = list(ex.map(impact, body[:3]))
base = (ch.get('built_at') or '')[:10]
base = '' if ch.get('against_index') else (ch.get('built_at') or '')[:10] # no commit recorded: nothing to name
if decls: lines.append(f"graph: this edit changed {len(decls)} declaration(s) in {rel}" + (f" (against the graph's commit {base})" if base and before is None else '') + " —")
for d, j in results:
head = f" {d.get('label') or d['kind']} {d['symbol']}" + (f" — {d['detail']}" if d.get('detail') else '')
Expand Down Expand Up @@ -263,6 +263,10 @@ def names(xs, k=4): return ', '.join(f"[{x['certainty']}] {x['display']} {x['at'
try: against = open(os.path.join(cwd, '.axiomcode', 'out', 'indexed-tree')).read().strip() or built
except OSError: against = built
if against != 'nogit' and subprocess.run(['git', 'diff', '--quiet', against, '--', rel], cwd=cwd, capture_output=True).returncode == 1: stale = f" — this file changed since the graph was built at {built[:10]}: lines are the graph's, not the file's"
elif against == 'nogit': # no commit recorded: the hash the index read it with
import ax_fresh
rec = ((ax_fresh.load_table(cwd) or {}).get('files') or {}).get(rel)
if rec and ax_fresh.digest(os.path.join(cwd, rel)) != rec[2]: stale = " — this file changed since the graph was indexed: lines are the graph's, not the file's"
except Exception: pass
if rows:
st = load_state(); ctx = set(context_ids())
Expand Down
27 changes: 23 additions & 4 deletions plugins/axiomcode/skills/axiomcode/scripts/ax_fresh.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,15 +504,34 @@ def _vnum(v):
except (TypeError, ValueError): return None

def _newer(old, impact, now_e):
"""newer_build's comparison: the table's built_by against this axiomcode's IMPACT_VERSION and engine (engine_id)"""
"""newer_build's comparison: the table's built_by against this axiomcode's IMPACT_VERSION and engine (engine_id).
AHEAD ON EITHER IS NEWER. A lower IMPACT_VERSION returned '' before the engines were compared, so a graph a 0.1.8
engine built, read by these scripts with AXIOMCODE_ENGINE naming a 0.1.3 checkout, was called "built by an older
axiomcode (0.1.8 -> 0.1.3)" and rebuilt with the older engine"""
oi, ni = _vnum(old.get('impact')), _vnum(impact)
if oi and ni and oi != ni:
return f"graph built by a newer axiomcode (IMPACT_VERSION {old.get('impact')}, this one has {impact})" if oi > ni else ''
if oi and ni and oi > ni:
return f"graph built by a newer axiomcode (IMPACT_VERSION {old.get('impact')}, this one has {impact})"
ov, nv = old.get('engine_version'), (now_e[0] if now_e else None)
if _vnum(ov) and _vnum(nv) and _vnum(ov) > _vnum(nv):
return f"graph built by a newer axiomcode (engine {ov}, this one is {nv})"
return ''

def _answering():
"""the engine checkout these scripts sit in, the one `axiomcode --version` reports; '' when they sit in none"""
w = H
while os.path.dirname(w) != w and not os.path.isfile(os.path.join(w, 'bin', 'axiomcode')): w = os.path.dirname(w)
return w if engine_ok(w) else ''

def _whose(eng):
"""WHICH ENGINE "THIS ONE" IS, when it is not the one answering: AXIOMCODE_ENGINE names another checkout, and a
rebuild here uses that one. Unsaid, "0.1.8 -> 0.1.3" read as a version going backwards under `axiomcode --version`
printing 0.1.8. '' when the engine compared is the checkout answering"""
a = _answering()
if not eng or not a or os.environ.get('AXIOMCODE_ENGINE') != eng or os.path.realpath(a) == os.path.realpath(eng): return ''
try: v = json.load(open(os.path.join(a, 'package.json'))).get('version', '?')
except (OSError, ValueError): v = '?'
return f"; compared with AXIOMCODE_ENGINE={eng}, which a rebuild here uses, not the axiomcode answering ({v} at {a})"

def built_by_state(repo, t=None):
"""(older, newer): what engine_change and newer_build return, found with one look at the engine. NEVER A DOWNGRADE
comes first: a graph a newer axiomcode built is newer whatever else differs, and only a graph that is not is judged
Expand All @@ -532,7 +551,7 @@ def built_by_state(repo, t=None):
langs = old.get('engine_langs')
now_e = engine_id(eng, langs) if eng else None
newer = _newer(old, impact, now_e)
if newer: return '', newer
if newer: return '', newer + _whose(eng)
diff = []
if now_e and old.get('engine_hash') and old.get('engine_stat') != now_e[2]:
h = _seen_hash(repo, now_e)
Expand Down
11 changes: 11 additions & 0 deletions plugins/axiomcode/skills/axiomcode/scripts/axiomcode-build
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,15 @@ set_base(){
# the commit the baseline follows, written LAST: `changed` waits while it differs from HEAD, so it must not say "moved"
# before the baseline's graph exists (keep_base_graph), or `changed` reads a baseline with no graph for it
commit_base(){ [ -n "$GD" ] && echo "$HEAD_SHA" > "$OUT/base-commit"; return 0; }
# THE BASELINE WITHOUT GIT (a mirror synced without .git, an export): no tree to record, so `changed` reads an edit as a
# file whose hash differs from the table an explicit index wrote (axiomcode-changed, since_index). Kept as base-files.json,
# since every build, a background refresh too, rewrites files.json: read from it, an edit left `changed` a minute after it
# was made. An explicit index moves it; a refresh keeps it; a build with git has a tree and drops it.
set_base_files(){
if [ -n "$GD" ]; then rm -f "$OUT/base-files.json"
elif [ -z "$KEEP_BASE" ] || [ ! -f "$OUT/base-files.json" ]; then [ -f "$OUT/files.json" ] && cp "$OUT/files.json" "$OUT/base-files.json"; fi
return 0
}
# THE BASELINE GRAPH. `changed`, `test-impact` and the change reports ask about an EDIT: which declarations it touched and
# who depended on them before it, a removed method's callers included. After a background refresh the graph no longer
# has the removed method, and its spans are in the new text. So the graph that describes the baseline is kept, moved
Expand Down Expand Up @@ -287,6 +296,7 @@ if [ -z "${AXIOMCODE_REINDEX:-}" ] && [ ! -f "$OUT/corrupt" ] && [ -f "$OUT/grap
fi
if [ ! -f "$OUT/corrupt" ] && [ -f "$OUT/graph.sqlite" ] && [ -f "$OUT/stamp" ] && [ ! -f "$OUT/partial" ] && AXIOMCODE_ENGINE="$ENGINE" python3 "$H/ax_fresh.py" uptodate "$REPO" "$LANGS" "${AXIOMCODE_SRC:-}" "${STAMP#"$PREFIX"}"; then
if [ "$(cat "$OUT/stamp")" != "$STAMP" ]; then echo "$STAMP" > "$OUT/stamp"; INDEXED_TREE="$OLD_INDEXED"; set_base "$OLD_INDEXED"; PREV=""; keep_base_graph; commit_base; fi
set_base_files
has_symbols "$OUT/graph.sqlite" || python3 "$H/axiomcode-index" "$REPO"
[ -f "$REPO/.axiomcode/engine" ] || printf '%s\n' "$ENGINE" > "$REPO/.axiomcode/engine" || true
echo "graph up to date (${LANGS//,/, }) at $OUT/graph.sqlite"; exit 0
Expand Down Expand Up @@ -457,6 +467,7 @@ publish_main(){
if [ -n "$OTHER_LANGS" ]; then S="${SOLVE#*,}"; for l in ${S//,/ }; do echo "$l $MODE"; done > "$PENDING"; fi # before the pointer: the first query may follow at once
point "$DB" || { restore; exit 1; }; rm -f "$OUT/.live.sqlite"
if [ -f "$OUT/.files.json.new" ]; then mv "$OUT/.files.json.new" "$OUT/files.json"; else rm -f "$OUT/files.json"; fi
set_base_files
# the graph now matches the files: record how long that took, and whether it included compiling the engine's rules (a
# one-time cost a refresh does not pay, so such a duration predicts nothing). With other languages still to come this
# is the time to the MAIN graph, which is what a query waits for.
Expand Down
46 changes: 42 additions & 4 deletions plugins/axiomcode/skills/axiomcode/scripts/axiomcode-changed
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ CODE_EXT = r'\.(java|ts|tsx|js|mjs|cjs|py|cs)$'
def is_git(repo):
return sh('git', 'rev-parse', '--git-dir', cwd=repo) is not None

def base_table(repo):
"""the file table a graph with no commit is diffed against: the one the last explicit index kept (base-files.json,
which a background refresh leaves as it is), else the current one"""
try: return json.load(open(os.path.join(repo, '.axiomcode', 'out', 'base-files.json')))
except (OSError, ValueError):
sys.path.insert(0, HERE); import ax_fresh
return ax_fresh.load_table(repo)

def resolve_range(repo, rng):
"""(old rev, new rev, note) for `--range`. `a..b` means what an agent asking about ITS commits means: what b added since
it forked from a, so the old side is `git merge-base a b`, as `a...b` already is. Diffing the two TIPS, as this did,
Expand Down Expand Up @@ -202,6 +210,20 @@ class Changed:
b = os.path.join(self.repo, '.axiomcode', 'out', 'base-tree')
b = open(b).read().strip() if os.path.exists(b) else ''
self.base_tree = b if b and sh('git', 'cat-file', '-e', b, cwd=self.repo) is not None else self.indexed_tree
# NO COMMIT RECORDED: a graph built where there was no git (a mirror synced without .git, an export) stamps 'nogit'
# and writes no tree. Read against HEAD, every uncommitted edit the index had READ came back as changed right after
# it (a fresh index of a dirty mirror: 208 declarations), and in a copy without git nothing could be answered. The
# file table the build wrote holds each file's hash as the parser read it: a file whose bytes still match is
# unchanged against the graph, whatever git says; one that differs, is new or is gone is the edit (since_index).
self.since_index = None
if (not self.built_at or self.built_at == 'nogit') and not self.base_tree:
sys.path.insert(0, HERE); import ax_fresh
t = base_table(self.repo)
if t and t.get('files'):
c = ax_fresh.changes(self.repo, t)
if c is not None:
self.since_index = set(c[0]) | set(c[1]) | set(c[2]); self.indexed_files = set(t['files'])
self.index_built = float(t.get('built') or 0)
# THE BASE MOVED UNDER THE BASELINE: a rebase, a pull, a checkout, a reset or a commit since it was set, and the
# refresher has not caught up (it builds HEAD's text first, which takes minutes on a dirty tree, and never runs
# with refresh off). Read against the old baseline, every change the new commits brought in (upstream's) came
Expand Down Expand Up @@ -419,6 +441,13 @@ class Changed:
# dropped here, an edit to them answered "no change" and test-impact "no test can be selected", which reads as
# "nothing to test". They are kept apart, named, and test-impact looks for the tests that load them.
lines = [l for l in (out or '').split('\n') if l.strip()]
if self.since_index is not None and mode == 'worktree':
# no commit to diff against: the files the table says changed since the index, and a file the table does not
# watch (a fixture, a schema) that git lists and that was written after the index
def later(l):
try: return os.path.getmtime(os.path.join(self.repo, l)) > self.index_built
except OSError: return False
lines = sorted(self.since_index | {l for l in lines if l not in self.indexed_files and not self.is_code(l) and later(l)})
self.outside = [l for l in lines if not self.is_code(l)]
return [l for l in lines if self.is_code(l)]
def is_code(self, rel):
Expand Down Expand Up @@ -1381,7 +1410,9 @@ def main(argv):
# function as added at line 1 and docstring words as methods. Without named files there is no answer to give: say so
# and say what to pass. With named files, each counts whole (below).
git = is_git(rrepo)
if not git and not (old_f or new_f) and not a:
sys.path.insert(0, HERE); import ax_fresh
# ...unless the graph's file table holds what the index read: an edit is a file that differs from it (Changed.since_index)
if not git and not (old_f or new_f) and not a and mode == 'worktree' and not (base_table(rrepo) or {}).get('files'):
msg = NO_GIT.format(repo=rrepo)
if as_json: print(json.dumps({'built_at': None, 'changed': [], 'notes': [], 'refused': msg, 'no_git': True}, indent=1))
else: print(msg)
Expand All @@ -1392,7 +1423,6 @@ def main(argv):
# A caller that already chose it (test-impact, the change hook) passes it in AXIOMCODE_GRAPH; either way it is the
# baseline's graph, and its spans are not mapped again.
baseline = None; fanout_empty = False
sys.path.insert(0, HERE); import ax_fresh
if mode != 'range' and not (old_f or new_f):
bg = ax_fresh.baseline_graph(rrepo); given = os.environ.get('AXIOMCODE_GRAPH')
# the dispatcher names the CURRENT graph of the language it is asking (AXIOMCODE_GRAPH_LANG): that is not a
Expand Down Expand Up @@ -1439,9 +1469,13 @@ def main(argv):
for rel in files:
if a and not git: # a named file, no git: every declaration in it
results += C.whole_file(rel, 'no git base to diff against'); continue
since_index = C.since_index is not None and mode == 'worktree' and rel in C.since_index
if since_index and not git: # changed since the index, no text of it before
results += C.whole_file(rel, 'changed since the index, and no git to diff its declarations against'); continue
old, new = C.texts(rel, mode, rng)
if old == new:
if a and whole: results += C.whole_file(rel, f"no edit against {'the index' if mode == 'staged' else 'the baseline'}: the file was named")
elif since_index: results += C.whole_file(rel, 'changed since the index, back to the text HEAD has: which declarations changed cannot be told')
continue
if flipped and not old.strip() and new.strip(): # a file the range adds: one line, not its decls reversed
results += C.new_file(rel, new, C.decl_spans(rel, mode), rel.endswith(('.py', '.pyi')) or bool(re.match(r'#![^\n]*\bpython', new)))
Expand Down Expand Up @@ -1470,14 +1504,16 @@ def main(argv):
# A BASELINE THAT HOLDS EDITS (an explicit index of an edited tree, or a rebuild run as one) hides them: said, with how
# to count them, rather than a bare "no change" over a real edit. Only when the question is the working tree as a whole
base_note = C.baseline_note() if mode == 'worktree' and not a and not (old_f or new_f) else ''
base_desc = (f"working tree against {C.built_at[:10] if C.built_at and C.built_at != 'nogit' else 'HEAD'}" if mode == 'worktree' else
by_index = C.since_index is not None and mode == 'worktree' and not (old_f or new_f)
base_desc = ("working tree against the files the graph was indexed from (no commit recorded)" if by_index else
f"working tree against {C.built_at[:10] if C.built_at and C.built_at != 'nogit' else 'HEAD'}" if mode == 'worktree' else
f"{rng} (from {C.range_old[:10]} to {C.range_new[:10]})" if mode == 'range' else mode)
sugg_line = (f"your commits are not in the working tree: HEAD is {suggest['commits']} commit(s) ahead of {suggest['ref']} — "
f"ask `changed --range {suggest['range']}` (MCP range='{suggest['range']}') for them") if suggest else None
mv = C.moved if mode in ('worktree', 'head') and not (old_f or new_f) else None
moved_json = dict(mv, note=moved_note(mv)) if mv else None
if as_json:
print(json.dumps({'built_at': C.built_at, 'changed': results, 'notes': [x[1] for x in notes], 'outside_index': sorted(outside),
print(json.dumps({'built_at': C.built_at, 'against_index': by_index, 'changed': results, 'notes': [x[1] for x in notes], 'outside_index': sorted(outside),
'range_base': (C.range_old if mode == 'range' else None), 'range_note': range_note, 'baseline_note': base_note,
'suggest_range': suggest, 'suggest_note': sugg_line, 'base_moved': moved_json}, indent=1))
return 3 if fanout_empty else 0
Expand All @@ -1489,6 +1525,8 @@ def main(argv):
if sugg_line: print(f"next: {sugg_line}")
return 3 if fanout_empty else 0
against = (f" — against HEAD {mv['new'][:10]} (the base moved: see the note)" if mv else
" — against the files the graph was indexed from (it recorded no commit): a file unchanged since the index is "
"not an edit; a changed one is read " + ("against HEAD" if git else "whole") if by_index else
f" — against the tree the graph was indexed from at the last `axiomcode index` (commit {C.built_at[:10]} plus the edits that were uncommitted then)" if C.tree_differs
else f" — against the graph's commit {C.built_at[:10]}" if C.built_at != 'nogit' else " — against HEAD (the graph was built before this was a git checkout)")
named = [e for e in results if e['kind'] == 'named']; shown = [e for e in results if e['kind'] != 'named']
Expand Down
Loading
Loading