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
2 changes: 1 addition & 1 deletion plugins/axiomcode/hooks/changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def impact(d):
for d, j in results:
hd = f" {d['kind']} {d['symbol']}" + (f" — {d['detail']}" if d.get('detail') else '')
if not j: lines.append(hd + " (impact unavailable)"); continue
con = j.get('contract', []); dr = sorted(j.get('direct', []), key=lambda x: (rank.get(x['certainty'], 9), x['display'])); rc = j.get('reached', []); ts = j.get('tests', [])
con = j.get('contract', []); dr = sorted((x for x in j.get('direct', []) if x.get('certainty') != 'alongside'), key=lambda x: (rank.get(x['certainty'], 9), x['display'])); rc = j.get('reached', []); ts = j.get('tests', [])
prod = [x for x in dr if x['role'] in ('produces', 'writes')]; reads = [x for x in dr if x['role'] in ('reads', 'uses')]
# THE TIER TRAVELS WITH THE ROW OR IT IS NOT READ. The printed command labels every row [resolved] /
# [by name] / [text]; this line dropped the label, so four rows of dataflow -- two of them reflective
Expand Down
6 changes: 4 additions & 2 deletions plugins/axiomcode/hooks/enrich.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,15 @@ def impact(d):
for d, j in results:
head = f" {d['kind']} {d['symbol']}" + (f" — {d['detail']}" if d.get('detail') else '')
if not j: lines.append(head + " (impact unavailable)"); continue
con = j.get('contract', []); dr = j.get('direct', []); rc = j.get('reached', []); ts = j.get('tests', [])
# `alongside` (same file, same type; no call, no reference) is not a user: an answer from before it had its own list
# still carries it in `direct`, so it is dropped here too
con = j.get('contract', []); dr = [x for x in j.get('direct', []) if x.get('certainty') != 'alongside']; rc = j.get('reached', []); ts = j.get('tests', [])
# ordered as ax_edges.DIRECT_ORDER and impact's CERT are: an edge the engine asserted outranks a
# name or a text match, and neither a hand-off nor a truncated fan-out outranks a resolved call.
rank = {'resolved': 0, 'one of a set': 1, 'registered': 2, 'capped set': 3, 'in scope': 4, 'by name': 5, 'text': 6}
dr = sorted(dr, key=lambda x: (rank.get(x['certainty'], 9), x['display']))
prod = [x for x in dr if x['role'] in ('produces', 'writes')]; reads = [x for x in dr if x['role'] in ('reads', 'uses')]
def names(xs, k=4): return ', '.join(f"{x['display']} {x['at'].split('/')[-1]}" for x in xs[:k]) + (f" … +{len(xs) - k}" if len(xs) > k else '')
def names(xs, k=4): return ', '.join(f"[{x['certainty']}] {x['display']} {x['at'].split('/')[-1]}" for x in xs[:k]) + (f" … +{len(xs) - k}" if len(xs) > k else '')
lines.append(head)
if con and d['kind'] in ('signature', 'field', 'type', 'removed'): lines.append(f" must change with it ({len(con)}): " + ', '.join(f"{x['display']} ({x['why']})" for x in con[:4]) + (' …' if len(con) > 4 else ''))
if prod: lines.append(f" produces / writes it ({len(prod)}): " + names(prod))
Expand Down
1 change: 1 addition & 0 deletions plugins/axiomcode/skills/axiomcode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ An answer's label is the **worst** rung on its route. Read it before acting on t
| `[by key]` | joined through a registration **string** (route, signal, CLI command) — not an edge |
| `[stubs it]` | a call written inside a mock's stub or verification (`when(m.f())`, `verify(m).f()`, `Setup(x => x.F())`, `Received().F()`): names it, runs none of it — never a test route, listed apart |
| `[in scope]` · `[by name]` · `[text]` | same name in the owner's scope · same name elsewhere (may be another thing) · text only |
| `[alongside]` | declared in the same type or file — no call, no reference; its own section (`alongside` in `--json`), never a dependent |

Below `[sound]` / `[one of a set]` the order is a tie-break, not a measured ranking. `[sound]` means the edges
connect, not that a test exercises the change.
Expand Down
1 change: 1 addition & 0 deletions plugins/axiomcode/skills/axiomcode/scripts/ax_grep.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def more(k, n=1): rest[k] = rest.get(k, 0) + n
for r in d.get('contract', []):
rows.append(('contract', site(code, r['at'], f"must change · {r['why']}{stale(r)}", r['display'])))
seen = set()
if d.get('alongside'): more('alongside (no call, no reference)', len(d['alongside']))
for r in d.get('direct', []):
cert = r.get('certainty') or 'resolved'
if cert == 'alongside': more('alongside (no call, no reference)'); continue
Expand Down
13 changes: 10 additions & 3 deletions plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact
Original file line number Diff line number Diff line change
Expand Up @@ -2237,7 +2237,11 @@ def main(argv):
# the sort key ends in x[0], the id: two distinct anon-class methods can share a
# display AND every other component of the key, and without the id the order is
# whatever the solver happened to emit — which is not the same between backends.
'direct': [{'id': c, 'display': g.disp(c), 'role': role, 'why': why, 'also': direct_also.get((c, _grp(role)), []), 'reasons': direct_reasons.get((c, _grp(role)), []), 'certainty': cert, 'at': loc, 'sites': n, 'for': sorted(direct_for[(c, _grp(role))])} for c, role, why, cert, loc, n in sorted(D, key=lambda x: (CERT[x[3]], x[1], g.disp(x[0]), x[2], x[4], x[0]))],
# AN `alongside` ROW IS NOT A DEPENDENT, so it never shares `direct` or a role with one. It sat
# there as role 'uses', and a reader keyed on role or section counted a same-file sibling as a
# user: a check passed on it, then failed when the type moved to another file with no edge changed.
'direct': [{'id': c, 'display': g.disp(c), 'role': role, 'why': why, 'also': direct_also.get((c, _grp(role)), []), 'reasons': direct_reasons.get((c, _grp(role)), []), 'certainty': cert, 'at': loc, 'sites': n, 'for': sorted(direct_for[(c, _grp(role))])} for c, role, why, cert, loc, n in sorted(D, key=lambda x: (CERT[x[3]], x[1], g.disp(x[0]), x[2], x[4], x[0])) if cert != 'alongside'],
'alongside': [{'id': c, 'display': g.disp(c), 'role': 'co-located', 'why': why, 'reasons': direct_reasons.get((c, _grp(role)), []), 'certainty': cert, 'at': loc, 'for': sorted(direct_for[(c, _grp(role))])} for c, role, why, cert, loc, n in sorted(D, key=lambda x: (g.disp(x[0]), x[2], x[4], x[0])) if cert == 'alongside'],
'reached': [{'id': m, 'display': g.disp(m), 'hops': d, 'for': sorted(reach_from[m]), 'at': g.loc(m), 'test': bool(g.sym[m]['is_test'])} for m, d in sorted(reached.items(), key=lambda x: (x[1], g.disp(x[0]), g.loc(x[0]), x[0]))],
'tests': [{'id': m, 'display': g.disp(m), 'owner': g.sym[m]['owner'], 'name': g.sym[m]['name'], 'hops': d, 'via': g.disp(fx) if fx else None, 'at': g.loc(m), 'certainty': test_cert.get(m), 'chain': [g.disp(x) for x in chains.get(m, [])],
**({'script': True, 'run': script_cmd(m)} if m in script_ids else {})} for m, (d, fx) in sorted(tests.items(), key=lambda kv: (kv[1][0], g.disp(kv[0]), g.loc(kv[0]), kv[0]))],
Expand Down Expand Up @@ -2274,7 +2278,10 @@ def main(argv):
# the hidden tail. Surest first, then named before anonymous, then the entries with the most sites
anon = lambda c: g.disp(c).split('.')[-1].startswith('<')
D.sort(key=lambda x: (CERT[x[3]], anon(x[0]), -(x[5] or 1), x[2], g.disp(x[0])))
groups = [('produces or writes it', [x for x in D if x[1] in ('produces', 'writes')]), ('reads or uses it', [x for x in D if x[1] in ('reads', 'uses')])]
# `alongside` rows are their own section, never counted under `reads or uses it`: no call, no reference, so no dependent
groups = [('produces or writes it', [x for x in D if x[1] in ('produces', 'writes') and x[3] != 'alongside']),
('reads or uses it', [x for x in D if x[1] in ('reads', 'uses') and x[3] != 'alongside']),
('declared alongside it, not a dependent', [x for x in D if x[3] == 'alongside'])]
# AN ENTRY POINT IS NOT "NOTHING". A route handler, a queue consumer or a CLI command is invoked BY THE
# FRAMEWORK, which is exactly WHY no client call site reaches it. Printing only "nothing the graph can see"
# for a live HTTP route reads as dead code and is the single most misleading line this command can emit
Expand Down Expand Up @@ -2350,7 +2357,7 @@ def main(argv):
# library base is the only signal `next:` states it as the condition it is (the `maybe` case below)
unmod_own = [x for x in unmod if not (' on its type ' in x[1] or x[1].endswith('which the graph does not contain'))]
unmod_sig = '; '.join(sig for _i, sig in unmod[:3]) + (f" (+{len(unmod) - 3} more)" if len(unmod) > 3 else '')
if not D: print("directly touches it: nothing the graph can see" + (" — the declaration is used only where it is declared, or through something the parser did not record" if not contract else ''))
if not [x for x in D if x[3] != 'alongside']: print("directly touches it: nothing the graph can see" + (" — the declaration is used only where it is declared, or through something the parser did not record" if not contract else ''))
# a throws clause is a contract: say what adding one to this method would cost, and who already handles the ones it has
n_callers = len({c for c, role, why, cert, loc, n in D if cert == 'resolved' and 'calls it' in why})
thrown = sorted({x[0] for x in res.get('target_throws', [])})
Expand Down
1 change: 1 addition & 0 deletions skills/axiomcode/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ An answer's label is the **worst** rung on its route. Read it before acting on t
| `[by key]` | joined through a registration **string** (route, signal, CLI command) — not an edge |
| `[stubs it]` | a call written inside a mock's stub or verification (`when(m.f())`, `verify(m).f()`, `Setup(x => x.F())`, `Received().F()`): names it, runs none of it — never a test route, listed apart |
| `[in scope]` · `[by name]` · `[text]` | same name in the owner's scope · same name elsewhere (may be another thing) · text only |
| `[alongside]` | declared in the same type or file — no call, no reference; its own section (`alongside` in `--json`), never a dependent |

Below `[sound]` / `[one of a set]` the order is a tie-break, not a measured ranking. `[sound]` means the edges
connect, not that a test exercises the change.
Expand Down
3 changes: 3 additions & 0 deletions skills/axiomcode/reference/schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,8 @@ sqlite3 -readonly $G "SELECT c2, count(*) FROM ext_call_site_unresolved GROUP BY
its `class Meta:` line.
- **Names.** Python `type_refs` and `decorations` keep only the last dotted segment; the full text is in
`ext_type_base_unresolved.c3` and `decorations.text`. String cells are CSV-escaped: match with `LIKE '%x%'`.
JavaScript `symbols`: a field (`this.x = …` in a constructor or constructor function, a class field) has its class as
`owner` (`Store.items`); a member with a computed key is named by the key as written (`Tagged.[Symbol.hasInstance]`);
an anonymous class expression takes the name it is bound to (`static Inner = class {…}` → `Outer.Inner`).
- **`ext_*` tables** have positional columns `c0…cN`; `SELECT description FROM schema_tables WHERE name = '<t>'`
names them.
19 changes: 19 additions & 0 deletions tests/cases/csharp/same-file-is-not-a-dependent/case.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{"lang": "csharp", "src": "src",
"checks": [
{"why": "a type declared in the same file that never names the target is not a dependent: it is listed apart, and `reads or uses it` counts only the real caller",
"run": ["impact", "Handler.Apply"],
"want": ["reads or uses it (1 callable(s): 1 resolved):", "declared alongside it, not a dependent (", "[alongside] Registrar.Wire"],
"avoid": ["reads or uses it (4 callable(s)"]},
{"why": "--json keeps it out of `direct` and gives it its own list and role, so a reader keyed on `direct` or `role` never counts it",
"run": ["impact", "Handler.Apply", "--json"],
"stdout_json": true,
"want": ["\"alongside\": [", "\"display\": \"Registrar.Wire\",\n \"role\": \"co-located\"", "\"display\": \"Caller.Run\",\n \"role\": \"uses\""],
"avoid": ["\"display\": \"Registrar.Wire\",\n \"role\": \"uses\""]},
{"why": "a const's same-file neighbours are not its readers; the method that reads it stays one (control)",
"run": ["impact", "Keys.Key"],
"want": ["reads or uses it (1 callable(s): 1 in scope):", "Keys.Read src/Wiring.cs:20 — reads it", "[alongside] Note.<primary-constructor>"],
"avoid": ["reads or uses it (4 callable(s)"]},
{"why": "control: with only alongside rows, nothing directly touches it, and the answer says so",
"run": ["impact", "Registrar.Wire"],
"want": ["directly touches it: nothing the graph can see", "declared alongside it, not a dependent ("],
"avoid": ["reads or uses it ("]}]}
6 changes: 6 additions & 0 deletions tests/cases/csharp/same-file-is-not-a-dependent/src/Caller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Demo;

public static class Caller
{
public static void Run() { Handler.Apply(new Ctx()); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>
23 changes: 23 additions & 0 deletions tests/cases/csharp/same-file-is-not-a-dependent/src/Wiring.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Demo;

public class Builder { }
public class Ctx { }

// two types in one file; neither names the other
public static class Registrar
{
public static void Wire(Builder b) { }
}

public static class Handler
{
public static void Apply(Ctx c) { }
}

public static class Keys
{
public const string Key = "k";
public static string Read() => Key;
}

public record Note(string Text);
11 changes: 11 additions & 0 deletions tests/enrich_lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
# two overloads in one file: one name to the reader, printed once
P + 'Pricer.java': ('package app.orders;\n\npublic class Pricer {\n public int quoteAll(int n) { return n; }\n\n'
' public int quoteAll(String s) { return quoteAll(s.length()); }\n}\n'),
# a constant with one reader, a sibling that does not read it, and a second class in the same file
P + 'Limits.java': ('package app.orders;\n\npublic class Limits {\n public static final int MAX_ITEMS = 3;\n\n'
' public int cap() { return MAX_ITEMS; }\n\n public int floor() { return 1; }\n}\n\n'
'class Spare {\n int spareCount() { return 2; }\n}\n'),
}
# two test stubs in modules whose paths sort before core/, with the same file name and line
for m in ('json', 'xml'):
Expand Down Expand Up @@ -175,6 +179,13 @@ def edit(session, text, hook='enrich.py', event='PostToolUse'):
out = fire(repo, 'u1', '', {}, 'changes.py', 'UserPromptSubmit')
check('the prompt-time report of a body edit is the same one line', 'body edit of OrderStore.findById' in out and 'reads / uses it' not in out, out)
open(f, 'w').write(orig)
# a declaration beside it in the file (a sibling, another class) is not a reader: `reads / uses it` counts readers only
f = os.path.join(repo, P + 'Limits.java')
orig = open(f).read()
out = edit('e5', orig.replace(' public static final int MAX_ITEMS = 3;\n\n', ''))
check('a removed constant lists its reader and not the declarations beside it',
'reads / uses it (1): ' in out and 'Limits.cap' in out and 'Limits.floor' not in out and 'Spare.spareCount' not in out, out)
open(f, 'w').write(orig)

# ── what a Read of a script says: a call in the text read is not an edge the text does not show ─────────────
PY = {
Expand Down
2 changes: 1 addition & 1 deletion tests/fastpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ def main(argv=None):
if a[k] != b[k]: print(f" {k}: fast={a[k]} rules={b[k]}")
bad += 1
else:
al = sorted({x['display'] for x in j.get('direct', []) if x.get('certainty') == ALONG})
al = sorted({x['display'] for x in j.get('alongside', [])})
print(f"ok {target!r}: {len(a['direct'])} direct, {a['reached']} reached — identical to the rules"
+ (f" (the rules also list {len(al)} `alongside` row(s), not compared: {', '.join(al)})" if al else ''))
finally:
Expand Down
Loading