Skip to content

Commit b271a22

Browse files
Merge pull request #1643 from AxiomCodeAI/fix/1392-mapper-context-seed
java: context orders equal-scoring entry points by the text file the question asks about (#1392)
2 parents 9ee9d59 + 16831fd commit b271a22

10 files changed

Lines changed: 124 additions & 10 deletions

File tree

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

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def rank_dirs(g, terms, depth=3, topk=DIR_EVIDENCE):
209209
return ranked
210210

211211

212-
def pick_seeds(g, scored, terms, want, named=()):
212+
def pick_seeds(g, scored, terms, want, named=(), prefer=frozenset()):
213213
"""The entry points, in four passes, each only filling what the one before left:
214214
215215
1. what the question NAMES: a declaration it spells out (`IRouter.RouteAsync`), a route it quotes
@@ -221,17 +221,28 @@ def pick_seeds(g, scored, terms, want, named=()):
221221
3. the best symbol for each term still uncovered, as before, so a task naming unrelated concepts still enters the
222222
graph at each of them;
223223
4. the best overall.
224+
225+
Equal scores are common: `findByCode` on a mapper, a repository, a service and a query class all match the same
226+
two words the same way. A tie used to fall to the graph's symbol ids, which are hashes that change with the
227+
directory a project is indexed in, so the same question on the same code picked a different four of nine from one
228+
copy to the next. A tie now goes first to `prefer` (a declaration bound by a text file of the kind the question
229+
asks about: the mapper method whose XML holds the SQL, when the question asks which SQL runs), then to the one
230+
whose own name holds more of the matched words, then to its name and place, which do not move.
224231
"""
225232
seeds, seen, covered = [], set(), set()
226233
def take(sid, why, sc):
227234
if sid in seen or len(seeds) >= want: return False
228235
seen.add(sid); seeds.append((sid, why, sc)); covered.update(scored.get(sid, (0, []))[1]); return True
229236
for sid, why in named:
230237
take(sid, why, scored.get(sid, (0, []))[0])
231-
ranked = sorted(scored.items(), key=lambda kv: -kv[1][0])
238+
# the task's words in the declaration's OWN name beat the same words reached through its owner or its path:
239+
# `OrderCreatedEvent` says more about "where is an order created" than `Order.getCreatedAt` does
240+
own = lambda sid: sum(t in ((g.sym.get(sid) or {}).get('name') or '').lower() for t in scored.get(sid, (0, []))[1])
241+
tie = lambda sid: (sid not in prefer, -own(sid), g.disp(sid) or '', g.loc(sid) or '', sid)
242+
ranked = sorted(scored.items(), key=lambda kv: (-kv[1][0],) + tie(kv[0]))
232243
floor = 0.05 * ranked[0][1][0] if ranked else 0
233244
multi = [(sid, sc, m) for sid, (sc, m) in ranked if len(m) >= 2 and sc >= floor]
234-
multi.sort(key=lambda r: (-len(r[2]), -r[1]))
245+
multi.sort(key=lambda r: (-len(r[2]), -r[1]) + tie(r[0]))
235246
for i, (sid, sc, m) in enumerate(multi):
236247
if len(seeds) >= want: break
237248
if not set(m) - covered: continue
@@ -241,7 +252,7 @@ def pick_seeds(g, scored, terms, want, named=()):
241252
if sib >= FLOW_FAN: break
242253
if m2 == m and sc2 >= 0.5 * sc and take(sid2, "matches '" + "' + '".join(m2) + "'", sc2): sib += 1
243254
best = {}
244-
for sid, (sc, matched) in scored.items():
255+
for sid, (sc, matched) in ranked:
245256
for t in matched:
246257
cur = best.get(t)
247258
if cur is None or sc > cur[0]: best[t] = (sc, sid)
@@ -585,6 +596,19 @@ def text_bindings(g, sids, under=(), want_ext=()):
585596
return [r for _k, r in sorted(out)]
586597

587598

599+
def asked_bound(g, scored, text, under=(), cap=300):
600+
"""{symbol id} among the multi-term matches that a text file of the kind the question asks about binds by name.
601+
Empty unless the question asks about such a file (SQL, configuration, a template): only then is the tree walked,
602+
and only then does it break a tie between equally scored entry points (pick_seeds)."""
603+
kinds = asked_exts(text)
604+
if not kinds or not NONSOURCE_ASK.search(text or ''): return frozenset()
605+
cands = sorted((sid for sid, (_sc, m) in scored.items() if len(m) >= 2), key=lambda s: -scored[s][0])[:cap]
606+
by_disp = collections.defaultdict(set)
607+
for sid in cands: by_disp[g.disp(sid)].add(sid)
608+
return frozenset(sid for f, _l, d, _n in text_bindings(g, cands, under=under, want_ext=kinds)
609+
if f.endswith(kinds) for sid in by_disp.get(d, ()))
610+
611+
588612
_SRC = {}
589613
def source_lines(repo, f):
590614
"""The file's lines, read once. The graph stores repo-relative paths; a run may be started anywhere."""
@@ -990,7 +1014,7 @@ def main(argv):
9901014
# themselves, and on Python every class has one, so they can be a third of this list while
9911015
# naming nowhere to go.
9921016
scored = {sid: v for sid, v in scored.items() if not is_synthetic(g.sym.get(sid, {}).get('name'))}
993-
seeds = pick_seeds(g, scored, terms, seeds_wanted, named)
1017+
seeds = pick_seeds(g, scored, terms, seeds_wanted, named, prefer=asked_bound(g, scored, body, text_dirs))
9941018

9951019
RESULT['terms'] = list(terms)
9961020
RESULT['scope'], RESULT['scope_offered'] = (scopes[0] if scopes else None), bool(scope_offered)
@@ -1123,10 +1147,10 @@ def main(argv):
11231147
sp = span_of.get((f, nm))
11241148
if not sp: continue
11251149
a, b = sp
1126-
body = source_lines(g.repo, f)[a - 1:min(b, a + SOURCE_LINES)]
1127-
if not body: continue
1150+
code = source_lines(g.repo, f)[a - 1:min(b, a + SOURCE_LINES)] # not `body`: that is the question
1151+
if not code: continue
11281152
print(f" {'':22} {nm} ({f}:{a})")
1129-
for k, ln in enumerate(body, a):
1153+
for k, ln in enumerate(code, a):
11301154
print(f" {'':22} {k:5} | {ln.rstrip()[:110]}")
11311155
if b > a + SOURCE_LINES: print(f" {'':22} {'':5} | … +{b - a - SOURCE_LINES} more line(s)")
11321156
if foot and budget > 0: print(foot + ", tests and benchmarks not listed")
@@ -1144,9 +1168,13 @@ def main(argv):
11441168
if g.has('overrides') and seed_ids:
11451169
ph = ','.join('?' * len(seed_ids))
11461170
impls = {r[0] for r in g.q(f"SELECT overriding_method_id FROM overrides WHERE method_id IN ({ph})", *seed_ids)} - seed_ids
1171+
# nearer through the implementation than through the seeds' own closure counts as nearer: the mapper call
1172+
# in the impl is one hop from it however far the closure walk happened to place it (hop 3 is outside the ring)
11471173
for x, d in neighbourhood(g, [i for i in impls if i in g.sym], cap=1).items():
1148-
if x not in seed_ids and x in g.sym and x not in depth: depth[x] = d + 1; ring.append(x)
1149-
near = list(seed_ids) + sorted(ring, key=lambda x: (not bodiless(g, x), depth[x], x))[:300]
1174+
if x in seed_ids or x not in g.sym or depth.get(x, d + 2) <= d + 1: continue
1175+
if depth.get(x, 3) > 2: ring.append(x)
1176+
depth[x] = d + 1
1177+
near = list(seed_ids) + sorted(ring, key=lambda x: (not bodiless(g, x), depth[x], g.disp(x) or '', x))[:300]
11501178
kinds = asked_exts(body)
11511179
binds = text_bindings(g, near, under=text_dirs, want_ext=kinds)
11521180
# the question named a kind of file (SQL: mapper XML or .sql): when some binding is of that kind, the others
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{"lang": "java",
2+
"checks": [
3+
{"why": "entry points that match the task's words equally are ordered by what the question asks for, not by symbol ids: a SQL question puts the mapper method its XML binds first, and lists that XML, though four other declarations match the same two words and sort before it by name",
4+
"run": ["context", "which SQL runs when an order is loaded by number"],
5+
"want": ["entry points", " OrderMapper.findByNumber ", "text files that name these declarations",
6+
"src/main/resources/mapper/OrderMapper.xml:3", "names findByNumber (OrderMapper.findByNumber)"],
7+
"avoid": ["WidgetMapper.xml"]},
8+
{"why": "near miss: the same words without asking about SQL get no text-file section, so the XML is not listed and no tree walk is paid for",
9+
"run": ["context", "where is an order loaded by number"],
10+
"want": ["entry points", "Order.toNumber"],
11+
"avoid": ["text files that name", "OrderMapper.xml", "WidgetMapper.xml"]},
12+
{"why": "--source with a SQL question prints the code and still lists the XML (the printed code once replaced the question text, and the text-file check crashed on it)",
13+
"run": ["context", "which SQL runs when an order is loaded by number", "--source"],
14+
"want": ["src/main/resources/mapper/OrderMapper.xml:3"],
15+
"avoid": ["Traceback", "WidgetMapper.xml"]}]}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package demo.order;
2+
3+
public class Order {
4+
public long id;
5+
public String ref;
6+
7+
public static String toNumber(String ref) { return ref.toUpperCase(); }
8+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package demo.order;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
public class OrderAudit {
7+
private final Map<String, Order> seen = new HashMap<>();
8+
9+
public Order findByNumber(String number) {
10+
return seen.get(Order.toNumber(number));
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package demo.order;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
public class OrderCache {
7+
private final Map<String, Order> seen = new HashMap<>();
8+
9+
public Order findByNumber(String number) {
10+
return seen.get(Order.toNumber(number));
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package demo.order;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
6+
public class OrderLabel {
7+
private final Map<String, Order> seen = new HashMap<>();
8+
9+
public Order findByNumber(String number) {
10+
return seen.get(Order.toNumber(number));
11+
}
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package demo.order;
2+
3+
import org.apache.ibatis.annotations.Mapper;
4+
5+
@Mapper
6+
public interface OrderMapper {
7+
Order findByNumber(String number);
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package demo.order;
2+
3+
public class OrderService {
4+
private final OrderMapper mapper;
5+
6+
public OrderService(OrderMapper mapper) { this.mapper = mapper; }
7+
8+
public Order resolve(String number) {
9+
return mapper.findByNumber(number);
10+
}
11+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<mapper namespace="demo.order.OrderMapper">
3+
<select id="findByNumber" resultType="demo.order.Order">select id, ref from orders where ref = #{number}</select>
4+
</mapper>
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<mapper namespace="demo.widget.WidgetMapper">
3+
<select id="findByNumber" resultType="demo.widget.Widget">select id from widgets where code = #{number}</select>
4+
</mapper>

0 commit comments

Comments
 (0)