Skip to content

Commit afa1c1a

Browse files
Merge pull request #1776 from AxiomCodeAI/apps/csharp/alongside-rows-presented-as-dependents
impact: rows declared alongside a target are listed apart, never as its dependents
2 parents ded4acb + 88aa157 commit afa1c1a

13 files changed

Lines changed: 86 additions & 7 deletions

File tree

‎plugins/axiomcode/hooks/changes.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def impact(d):
7575
for d, j in results:
7676
hd = f" {d['kind']} {d['symbol']}" + (f" — {d['detail']}" if d.get('detail') else '')
7777
if not j: lines.append(hd + " (impact unavailable)"); continue
78-
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', [])
78+
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', [])
7979
prod = [x for x in dr if x['role'] in ('produces', 'writes')]; reads = [x for x in dr if x['role'] in ('reads', 'uses')]
8080
# THE TIER TRAVELS WITH THE ROW OR IT IS NOT READ. The printed command labels every row [resolved] /
8181
# [by name] / [text]; this line dropped the label, so four rows of dataflow -- two of them reflective

‎plugins/axiomcode/hooks/enrich.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,15 @@ def impact(d):
195195
for d, j in results:
196196
head = f" {d['kind']} {d['symbol']}" + (f" — {d['detail']}" if d.get('detail') else '')
197197
if not j: lines.append(head + " (impact unavailable)"); continue
198-
con = j.get('contract', []); dr = j.get('direct', []); rc = j.get('reached', []); ts = j.get('tests', [])
198+
# `alongside` (same file, same type; no call, no reference) is not a user: an answer from before it had its own list
199+
# still carries it in `direct`, so it is dropped here too
200+
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', [])
199201
# ordered as ax_edges.DIRECT_ORDER and impact's CERT are: an edge the engine asserted outranks a
200202
# name or a text match, and neither a hand-off nor a truncated fan-out outranks a resolved call.
201203
rank = {'resolved': 0, 'one of a set': 1, 'registered': 2, 'capped set': 3, 'in scope': 4, 'by name': 5, 'text': 6}
202204
dr = sorted(dr, key=lambda x: (rank.get(x['certainty'], 9), x['display']))
203205
prod = [x for x in dr if x['role'] in ('produces', 'writes')]; reads = [x for x in dr if x['role'] in ('reads', 'uses')]
204-
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 '')
206+
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 '')
205207
lines.append(head)
206208
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 ''))
207209
if prod: lines.append(f" produces / writes it ({len(prod)}): " + names(prod))

‎plugins/axiomcode/skills/axiomcode/SKILL.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ An answer's label is the **worst** rung on its route. Read it before acting on t
6666
| `[by key]` | joined through a registration **string** (route, signal, CLI command) — not an edge |
6767
| `[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 |
6868
| `[in scope]` · `[by name]` · `[text]` | same name in the owner's scope · same name elsewhere (may be another thing) · text only |
69+
| `[alongside]` | declared in the same type or file — no call, no reference; its own section (`alongside` in `--json`), never a dependent |
6970

7071
Below `[sound]` / `[one of a set]` the order is a tie-break, not a measured ranking. `[sound]` means the edges
7172
connect, not that a test exercises the change.

‎plugins/axiomcode/skills/axiomcode/scripts/ax_grep.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def more(k, n=1): rest[k] = rest.get(k, 0) + n
9494
for r in d.get('contract', []):
9595
rows.append(('contract', site(code, r['at'], f"must change · {r['why']}{stale(r)}", r['display'])))
9696
seen = set()
97+
if d.get('alongside'): more('alongside (no call, no reference)', len(d['alongside']))
9798
for r in d.get('direct', []):
9899
cert = r.get('certainty') or 'resolved'
99100
if cert == 'alongside': more('alongside (no call, no reference)'); continue

‎plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2237,7 +2237,11 @@ def main(argv):
22372237
# the sort key ends in x[0], the id: two distinct anon-class methods can share a
22382238
# display AND every other component of the key, and without the id the order is
22392239
# whatever the solver happened to emit — which is not the same between backends.
2240-
'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]))],
2240+
# AN `alongside` ROW IS NOT A DEPENDENT, so it never shares `direct` or a role with one. It sat
2241+
# there as role 'uses', and a reader keyed on role or section counted a same-file sibling as a
2242+
# user: a check passed on it, then failed when the type moved to another file with no edge changed.
2243+
'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'],
2244+
'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'],
22412245
'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]))],
22422246
'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, [])],
22432247
**({'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]))],
@@ -2274,7 +2278,10 @@ def main(argv):
22742278
# the hidden tail. Surest first, then named before anonymous, then the entries with the most sites
22752279
anon = lambda c: g.disp(c).split('.')[-1].startswith('<')
22762280
D.sort(key=lambda x: (CERT[x[3]], anon(x[0]), -(x[5] or 1), x[2], g.disp(x[0])))
2277-
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')])]
2281+
# `alongside` rows are their own section, never counted under `reads or uses it`: no call, no reference, so no dependent
2282+
groups = [('produces or writes it', [x for x in D if x[1] in ('produces', 'writes') and x[3] != 'alongside']),
2283+
('reads or uses it', [x for x in D if x[1] in ('reads', 'uses') and x[3] != 'alongside']),
2284+
('declared alongside it, not a dependent', [x for x in D if x[3] == 'alongside'])]
22782285
# AN ENTRY POINT IS NOT "NOTHING". A route handler, a queue consumer or a CLI command is invoked BY THE
22792286
# FRAMEWORK, which is exactly WHY no client call site reaches it. Printing only "nothing the graph can see"
22802287
# for a live HTTP route reads as dead code and is the single most misleading line this command can emit
@@ -2350,7 +2357,7 @@ def main(argv):
23502357
# library base is the only signal `next:` states it as the condition it is (the `maybe` case below)
23512358
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'))]
23522359
unmod_sig = '; '.join(sig for _i, sig in unmod[:3]) + (f" (+{len(unmod) - 3} more)" if len(unmod) > 3 else '')
2353-
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 ''))
2360+
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 ''))
23542361
# a throws clause is a contract: say what adding one to this method would cost, and who already handles the ones it has
23552362
n_callers = len({c for c, role, why, cert, loc, n in D if cert == 'resolved' and 'calls it' in why})
23562363
thrown = sorted({x[0] for x in res.get('target_throws', [])})

‎skills/axiomcode/SKILL.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ An answer's label is the **worst** rung on its route. Read it before acting on t
6666
| `[by key]` | joined through a registration **string** (route, signal, CLI command) — not an edge |
6767
| `[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 |
6868
| `[in scope]` · `[by name]` · `[text]` | same name in the owner's scope · same name elsewhere (may be another thing) · text only |
69+
| `[alongside]` | declared in the same type or file — no call, no reference; its own section (`alongside` in `--json`), never a dependent |
6970

7071
Below `[sound]` / `[one of a set]` the order is a tie-break, not a measured ranking. `[sound]` means the edges
7172
connect, not that a test exercises the change.

‎skills/axiomcode/reference/schema.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,8 @@ sqlite3 -readonly $G "SELECT c2, count(*) FROM ext_call_site_unresolved GROUP BY
9999
its `class Meta:` line.
100100
- **Names.** Python `type_refs` and `decorations` keep only the last dotted segment; the full text is in
101101
`ext_type_base_unresolved.c3` and `decorations.text`. String cells are CSV-escaped: match with `LIKE '%x%'`.
102+
JavaScript `symbols`: a field (`this.x = …` in a constructor or constructor function, a class field) has its class as
103+
`owner` (`Store.items`); a member with a computed key is named by the key as written (`Tagged.[Symbol.hasInstance]`);
104+
an anonymous class expression takes the name it is bound to (`static Inner = class {…}` → `Outer.Inner`).
102105
- **`ext_*` tables** have positional columns `c0…cN`; `SELECT description FROM schema_tables WHERE name = '<t>'`
103106
names them.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{"lang": "csharp", "src": "src",
2+
"checks": [
3+
{"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",
4+
"run": ["impact", "Handler.Apply"],
5+
"want": ["reads or uses it (1 callable(s): 1 resolved):", "declared alongside it, not a dependent (", "[alongside] Registrar.Wire"],
6+
"avoid": ["reads or uses it (4 callable(s)"]},
7+
{"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",
8+
"run": ["impact", "Handler.Apply", "--json"],
9+
"stdout_json": true,
10+
"want": ["\"alongside\": [", "\"display\": \"Registrar.Wire\",\n \"role\": \"co-located\"", "\"display\": \"Caller.Run\",\n \"role\": \"uses\""],
11+
"avoid": ["\"display\": \"Registrar.Wire\",\n \"role\": \"uses\""]},
12+
{"why": "a const's same-file neighbours are not its readers; the method that reads it stays one (control)",
13+
"run": ["impact", "Keys.Key"],
14+
"want": ["reads or uses it (1 callable(s): 1 in scope):", "Keys.Read src/Wiring.cs:20 — reads it", "[alongside] Note.<primary-constructor>"],
15+
"avoid": ["reads or uses it (4 callable(s)"]},
16+
{"why": "control: with only alongside rows, nothing directly touches it, and the answer says so",
17+
"run": ["impact", "Registrar.Wire"],
18+
"want": ["directly touches it: nothing the graph can see", "declared alongside it, not a dependent ("],
19+
"avoid": ["reads or uses it ("]}]}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Demo;
2+
3+
public static class Caller
4+
{
5+
public static void Run() { Handler.Apply(new Ctx()); }
6+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net8.0</TargetFramework>
4+
</PropertyGroup>
5+
</Project>

0 commit comments

Comments
 (0)