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
4 changes: 3 additions & 1 deletion plugins/axiomcode/skills/axiomcode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Search with grep as usual; the graph answers what grep cannot. Use the MCP tools
|---|---|---|
| where is the code for this task? | your own search (grep), then bring the name here | — |
| who calls X, what does changing it reach, which tests? | `impact(name)` | `axiomcode impact <name>` |
| what is the value of constant X, and who reads it? | `impact(name)` | `axiomcode impact <name>` |
| what do my uncommitted edits reach? | `impact()` | `axiomcode impact` |
| how does A reach B? | `path(start, end)` | `axiomcode path <A> <B>` |
| which tests do my edits need, and how do I run them? | `tests()` | `axiomcode tests` |
Expand Down Expand Up @@ -44,7 +45,8 @@ from an empty answer.

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

## path

Expand Down
28 changes: 21 additions & 7 deletions plugins/axiomcode/skills/axiomcode/scripts/ax_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def registrations(q, site_file=None):
sf = site_file or (lambda x: x)
lits = {}
if _has(q, 'literals'):
for v, f, l in q("SELECT value, file, line FROM literals WHERE line > 0 AND value IS NOT NULL"):
for v, f, l in _string_literals(q):
lits.setdefault((f, l), []).append(v)
# the declarations a name identifies uniquely: only those can be named as the registered declaration, because
# a site names a VALUE by identifier and two callables of one name would each claim the other's registration
Expand Down Expand Up @@ -107,7 +107,7 @@ def route_site_lines(q, site_file=None):
return {}
lits = {}
if _has(q, 'literals'):
for v, f, l in q("SELECT value, file, line FROM literals WHERE line > 0 AND value IS NOT NULL"):
for v, f, l in _string_literals(q):
lits.setdefault((f, l), []).append(v)
return _route_links(q, site_file or (lambda x: x), lits, set())[2]

Expand Down Expand Up @@ -628,6 +628,20 @@ def _has(q, t):
return bool(q("SELECT 1 FROM sqlite_master WHERE name=?", t))


def _span_string_literals(q, f, a, b):
"""the STRING literals inside one file span, kind-filtered the same way _string_literals is"""
try: return q("SELECT value, file, line FROM literals WHERE file = ? AND line BETWEEN ? AND ? AND kind = 'string'", f, a, b)
except Exception: return q("SELECT value, file, line FROM literals WHERE file = ? AND line BETWEEN ? AND ?", f, a, b)


def _string_literals(q):
"""the literals rows that are STRINGS, as (value, file, line). A v8 index also carries numbers and
booleans, which are never registration keys and would join everything (`"1"` matches every retry
count); a pre-v8 or degraded index has no kind column, and there every row is a string."""
try: return q("SELECT value, file, line FROM literals WHERE line > 0 AND value IS NOT NULL AND kind = 'string'")
except Exception: return q("SELECT value, file, line FROM literals WHERE line > 0 AND value IS NOT NULL")


# ── the DECORATION path: the key is written at the `@`, and the owner is recorded ────────────────────────────
# `registrations()` above skips DECORATOR_CALL sites deliberately, because a decoration is not a call that hands a
# value over. It is the other half of the same idea and it carries BETTER evidence: the index records which
Expand Down Expand Up @@ -814,7 +828,7 @@ def literal_verbs(q, at, site_file=None):
short = re.sub(r'Async$', '', (n or '').split('.')[-1].split('<')[0]).upper()
if short in _VERBS: calls[sf(f) if f else ''].append((a, b or a, short))
out = set()
for v, f, l in q("SELECT value, file, line FROM literals WHERE line > 0 AND value IS NOT NULL"):
for v, f, l in _string_literals(q):
if not (isinstance(v, str) and v.startswith('/')): continue
f2 = sf(f) if f else ''
hold = [(b - a, -a, verb) for a, b, verb in calls.get(f2, ()) if a <= l <= b]
Expand Down Expand Up @@ -857,7 +871,7 @@ def value_route_registrations(q, site_file=None):
names_at.setdefault((f, l), set()).update(routed[(f, n)])
import re
out = []
for v, f, l in q("SELECT value, file, line FROM literals WHERE line > 0 AND value IS NOT NULL"):
for v, f, l in _string_literals(q):
if not (isinstance(v, str) and 0 < len(v) < 160):
continue
# A RESOURCE IS NOT A ROUTE. Measured on the JVM parser: the pair fired on
Expand Down Expand Up @@ -1118,7 +1132,7 @@ def written(text, token):
if not _KEY_POS.match(text, mm.end()): return True
return False
if _has(q, 'literals'):
for v, f, l in q("SELECT value, file, line FROM literals WHERE line > 0 AND value IS NOT NULL"):
for v, f, l in _string_literals(q):
if v not in keys or (f, l) in cpos: continue
L = read(f)
text = L[l - 1] if L and l <= len(L) else None
Expand Down Expand Up @@ -1187,7 +1201,7 @@ def key_writes(q, table_keys=None):
the table's own key position or the constant's declaration)."""
if table_keys is None:
table_keys = {r[4] for r in table_registrations(q)}
rows = [(v, f, l) for v, f, l in q("SELECT value, file, line FROM literals WHERE line > 0 AND value IS NOT NULL")
rows = [(v, f, l) for v, f, l in _string_literals(q)
if v not in table_keys] if _has(q, 'literals') else []
return rows + table_key_writes(q, table_keys)

Expand Down Expand Up @@ -1313,6 +1327,6 @@ def _sends_request(q, m):
ph = ','.join('?' * len(REQUEST_CALLS))
if q(f"SELECT 1 FROM call_sites WHERE caller_id = ? AND callee_name IN ({ph}) LIMIT 1", m, *sorted(REQUEST_CALLS)): return True
if _has(q, 'literals'):
for (v,) in q("SELECT value FROM literals WHERE file = ? AND line BETWEEN ? AND ?", f, a, b):
for v, _f, _l in _span_string_literals(q, f, a, b):
if isinstance(v, str) and re.fullmatch(r'/[\w\-./{}:%]*', v): return True
return False
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,10 @@ def route_seeds(g, text):
for r in g.q("SELECT owner_id, text, file, line FROM decorations WHERE text LIKE '%/%' OR text LIKE '%\"%'"):
for v in re.findall(r'"([^"]*)"', r['text'] or ''): lits.append((v, r['file'], r['line'], r['owner_id']))
if asked and g.has('literals'):
for r in g.q("SELECT value, file, line FROM literals WHERE value LIKE '%/%' OR length(value) < 40"):
# strings only: a route is never a number, and the v8 index carries numbers and booleans too
try: lit_rows = g.q("SELECT value, file, line FROM literals WHERE (value LIKE '%/%' OR length(value) < 40) AND kind = 'string'")
except Exception: lit_rows = g.q("SELECT value, file, line FROM literals WHERE value LIKE '%/%' OR length(value) < 40")
for r in lit_rows:
lits.append(((r['value'] or '').strip('"'), r['file'], r['line'], None))
by_file = collections.defaultdict(set)
for v, f, _l, _o in lits: by_file[f].add(tuple(_route_parts(v)))
Expand Down
24 changes: 19 additions & 5 deletions plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ class Impact:
if d and d[0] == 'field' and kind in (None, 'field'):
row = self.fields.get(d[1]['rowid']) or d[1]
self.WHY['field'] = {'step': 'file and line: a field declared there', 'won': f"the line declares {row['display']} and no callable of its own", 'ids': [], 'rows': [(row['display'], f"{row['file']}:{row['line']}")]}
return [('field', f"{row['kind']} {row['display']} (at {s})", [row])]
return [('field', f"{row['kind']} {row['display']}{self.const_value(row)} (at {s})", [row])]
if d and d[0] == 'type' and kind in (None, 'type'):
self.WHY['type'] = {'step': 'file and line: a type declared there', 'won': f"the line is the header of type {d[1]['display']}", 'ids': [d[1]['id']], 'rows': None}
return [('type', f"{d[1]['kind']} {d[1]['display']} (at {s})", [d[1]['id']])]
Expand Down Expand Up @@ -532,7 +532,7 @@ class Impact:
if hit: return [('method', verb + (f" ({len(hit)} declarations)" if len(hit) > 1 else ''), [x['id'] for x in hit])]
# …and a private FIELD (`Vault.#secret`, `#secret`) the same way: rewritten, it asked for `Vault..secret`
rows = self.field_rows(verb) if kind in (None, 'field') and re.search(r'(^|\.)#[A-Za-z_$]', verb) else []
if rows: return [('field', f"{rows[0]['kind']} {rows[0]['display']}" + (f" (+{len(rows)-1} declarations of that name)" if len(rows) > 1 else ''), rows)]
if rows: return [('field', f"{rows[0]['kind']} {rows[0]['display']}{self.const_value(rows[0])}" + (f" (+{len(rows)-1} declarations of that name)" if len(rows) > 1 else ''), rows)]
base = re.sub(r'\(.*\)$', '', s).replace('#', '.').strip('.')
if not (self.field_rows(base) or self.types(base, soft=True) or self.methods(base, soft=True)): base = base.replace('$', '.') # Outer$Inner — unless $ is part of the name ($Gson$Types)
m0 = re.match(r'^(.+)\.<new>$', base)
Expand All @@ -552,7 +552,7 @@ class Impact:
if kind in (None, 'field'):
rows = self.field_rows(base)
if rows: self.WHY['field'] = self.why_field(base, rows)
if rows: out.append(('field', f"{rows[0]['kind']} {rows[0]['display']}" + (f" (+{len(rows)-1} declarations of that name)" if len(rows) > 1 else ''), rows))
if rows: out.append(('field', f"{rows[0]['kind']} {rows[0]['display']}{self.const_value(rows[0])}" + (f" (+{len(rows)-1} declarations of that name)" if len(rows) > 1 else ''), rows))
if kind in (None, 'type'):
tids = self.types(base, soft=True)
if tids: self.WHY['type'] = self._why_t.get(base)
Expand Down Expand Up @@ -692,6 +692,16 @@ class Impact:
f['file'], r['line'], f['line'], f['line']): continue
return True
return False
def const_value(self, row):
"""`` = 5`` — the scalar the v8 index linked to this declaration's line (literals.name), so the
answer to `impact MAX_ITEMS` states the value a spec would print, not only who reads it. Empty
on a pre-v8 index, a non-scalar initializer, or any line the linker refused as ambiguous."""
try:
for v, k in self.g.q("SELECT value, kind FROM literals WHERE name = ? AND file = ? AND line = ? LIMIT 1",
row['display'], row['file'], row['line']):
return f" = '{v}'" if k == 'string' else f" = {v}"
except Exception: pass
return ''
def field_rows(self, s):
# `file.js:12` — the field or const DECLARED on that line. Split on `.` it named a field `js:12`, so a const was
# the one declaration a file:line could not target, and its bare name answered for every const so named
Expand Down Expand Up @@ -3077,8 +3087,12 @@ def main(argv):
if tests_only: sys.stdout = io.StringIO()
for k, lab, pay in targets:
if k in ('method', 'field', 'type'):
nm = lab.split()[-1].split('(')[0].split('.')[-1]
n = len(g.q("SELECT 1 FROM literals WHERE value = ?", nm)) if g.has('literals') else 0
# a const's label carries its value (`const MAX_ITEMS = 5`): the name is the word before `=`
nm = lab.split(' = ')[0].split()[-1].split('(')[0].split('.')[-1]
if g.has('literals'):
try: n = len(g.q("SELECT 1 FROM literals WHERE value = ? AND kind = 'string'", nm))
except Exception: n = len(g.q("SELECT 1 FROM literals WHERE value = ?", nm))
else: n = 0
d = len(g.q("SELECT 1 FROM decorations WHERE text LIKE ?", f'%"{nm}"%')) if g.has('decorations') else 0
if n + d > 1: print(f" the name {nm} is also written as a string in {n + d} place(s) ({STRING_BINDS.get(target_ext(g, pay), STRING_BINDS[''])}): ask for it quoted, `impact '\"{nm}\"'`, to get those")
break
Expand Down
Loading
Loading