Files
reliquary/plugins/orbit/skills/orbit/scripts/traitcov.py
T
g4borg f1de89e869 🤖 Add orbit prototype plugin for Rust code review
Standalone plugin with one skill: when to use orbit and when grep, a
mandatory preflight for stale indexes and blind regions (cfg(test),
features, macros, dyn dispatch), a scripted review pass over isolated
worktree DBs, and a JSONL feedback log for mining real usage later.
Registered in the marketplace as a prototype.
2026-09-29 19:52:59 +02:00

108 lines
6.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Recipe 3: trait-impl coverage for traits touched by a diff (or named with --trait).
Usage: traitcov.py <env-file> <defdiff.json> [--trait Name ...] [--all-traits]
For each trait: its methods at tip (+/- vs base), implementors from three sources, and per
implementor which trait methods it defines (and whether the diff touched them):
E = EXTENDS edge (orbit; only when `impl Trait for Type` sits in the type's own file)
G = `git grep 'impl … Trait … for Type'` at tip (text; sees cfg-gated/test impls too)
H = method-name overlap heuristic (orbit; >= 60% of trait methods defined on the type)
Implementors only in G are cross-file impls, impls on external types, or cfg-gated/test impls.
"""
import collections, json, re, subprocess, sys
sys.path.insert(0, __file__.rsplit('/', 1)[0])
from ob import load_env, q, git
from regions import cfg_regions, region_of
env = load_env(sys.argv[1]); dd = json.load(open(sys.argv[2]))
DB, BW, TW, REPO, B, T = env['DB'], env['BASE_WT'], env['TIP_WT'], env['REPO'], env['BASE_SHA'], env['TIP_SHA']
names = [sys.argv[i + 1] for i, a in enumerate(sys.argv) if a == '--trait']
D = {}
for side, wt in (('tip', TW), ('base', BW)):
D[side] = q("SELECT id, fqn, name, definition_type k, file_path f, start_line s, end_line e FROM gl_definition WHERE file_path LIKE '%.rs'", repo=wt, db=DB)
traits = {d['fqn']: d for d in D['tip'] if d['k'] == 'Trait'}
btraits = {d['fqn']: d for d in D['base'] if d['k'] == 'Trait'}
touched = set(dd['touched_tip'])
if '--all-traits' in sys.argv: sel = set(traits)
elif names: sel = {f for f, d in traits.items() if d['name'] in names}
else:
sel = set()
for r in dd['rows']:
par = r['fqn'].rsplit('::', 1)[0]
if r['k'] == 'Trait' and r['fqn'] in traits: sel.add(r['fqn'])
if par in traits and r['op'] != '~' or par in traits and r['op'] == '~': sel.add(par)
for r in dd['rows']:
if r['k'] == 'Trait' and r['op'] == '-': print(f"- Trait {r['fqn']} REMOVED — check leftover impls with: git grep -n 'impl .*{r['name']}' {T[:8]}")
ext = collections.defaultdict(set)
for e in q("SELECT s.fqn src, t.fqn tgt FROM gl_edge e JOIN gl_definition s ON s.id=e.source_id JOIN gl_definition t ON t.id=e.target_id WHERE e.relationship_kind='EXTENDS'", repo=TW, db=DB):
ext[e['tgt']].add(e['src'])
methods_of = collections.defaultdict(dict)
for d in D['tip']:
if d['k'] in ('Method', 'AssociatedFunction', 'Constant', 'TypeAlias'):
methods_of[d['fqn'].rsplit('::', 1)[0]][d['name']] = d
bmethods_of = collections.defaultdict(dict)
for d in D['base']:
if d['k'] in ('Method', 'AssociatedFunction'):
bmethods_of[d['fqn'].rsplit('::', 1)[0]][d['name']] = d
types_by_name = collections.defaultdict(list)
for d in D['tip']:
if d['k'] in ('Struct', 'Enum', 'TypeAlias', 'Trait'): types_by_name[d['name']].append(d)
_src = {}
def regions(f):
if f not in _src:
try: _src[f] = cfg_regions(open(f"{TW}/{f}", encoding='utf-8', errors='replace').read().split('\n'))
except FileNotFoundError: _src[f] = []
return _src[f]
stats = collections.Counter()
for tf in sorted(sel):
t = traits[tf]; tm = methods_of.get(tf, {}); bm = bmethods_of.get(tf, {})
added = sorted(set(tm) - set(bm)); removed = sorted(set(bm) - set(tm))
sigchg = sorted(r['name'] for r in dd['rows'] if r['op'] == 'S' and r['fqn'].rsplit('::', 1)[0] == tf)
print(f"Trait {tf} [{t['f']}:{t['s']}] methods={len(tm)}" + (f" +{added}" if added else '') + (f" -{removed}" if removed else '') + (f" sig~{sigchg}" if sigchg else ''))
# G: text impls at tip
try: g = git(REPO, 'grep', '-nE', rf'^\s*(unsafe\s+)?impl\b.*\b{t["name"]}\b(<[^>]*>)?\s+for\s+', T, '--', '*.rs')
except subprocess.CalledProcessError: g = ''
G = {}
for l in g.splitlines():
_, f, ln, txt = l.split(':', 3)
m = re.search(rf'\bfor\s+&?(\'\w+\s+)?(mut\s+)?(dyn\s+)?([\w:]+)', txt)
if not m: continue
ty = m.group(4).split('::')[-1]; reg = region_of(regions(f), int(ln))
G[(ty, f)] = f"cfg-{reg[2]}" if reg else ''
E = {x.split('::')[-1]: x for x in ext.get(tf, set())}
# H: overlap heuristic
H = {}
if tm:
for owner, ms in methods_of.items():
if owner == tf: continue
ov = len(set(ms) & set(tm))
if ov >= max(2, 0.6 * len(tm)): H[owner.split('::')[-1]] = owner
impl_types = sorted(set(E) | {ty for ty, _ in G} | set(H))
for ty in impl_types:
src = ''.join(c for c, ok in (('E', ty in E), ('G', any(k[0] == ty for k in G)), ('H', ty in H)) if ok)
gfiles = [f for (tt, f) in G if tt == ty]; gates = {G[(ty, f)] for f in gfiles if G[(ty, f)]}
owners = [E.get(ty)] if ty in E else ([H[ty]] if ty in H else [d['fqn'] for d in types_by_name.get(ty, [])])
owners = [o for o in owners if o]
have = set(); tch = set()
for o in owners:
for n, d in methods_of.get(o, {}).items():
if n in tm: have.add(n); tch |= {n} if d['id'] in touched else set()
missing = sorted(set(added) - have) if added else []
cover = f"defines {len(have)}/{len(tm)}" if owners else "type not indexed (external/gated)"
flag = ''
if missing:
# grep fallback: look for `fn <m>` in the impl files (text; sees gated code)
txt = {m: any(re.search(rf'\bfn\s+{m}\b', open(f"{TW}/{f}", errors='replace').read()) for f in gfiles) for m in missing}
ok = [m for m, v in txt.items() if v]; bad = [m for m, v in txt.items() if not v]
if ok: flag += f" new method(s) {ok} found by text in impl file (orbit can't see it)"
if bad: flag += f" !! new method(s) {bad} NOT found in impl file (default impl? not updated?)"
print(f" [{src:3s}] {ty} {cover}{' touched:'+','.join(sorted(tch)) if tch else ''}"
f"{' impl-in: '+','.join(sorted(set(gfiles)))[:120] if gfiles and 'E' not in src else ''}{' '+','.join(gates) if gates else ''}{flag}")
stats['G'] += any(k[0] == ty for k in G); stats['E'] += ty in E; stats['H'] += ty in H
stats['EG'] += (ty in E and any(k[0] == ty for k in G)); stats['HG'] += (ty in H and any(k[0] == ty for k in G))
stats['H_only'] += (ty in H and not any(k[0] == ty for k in G))
print(f"# implementors: grep={stats['G']} extends={stats['E']} (∩grep {stats['EG']}) heuristic={stats['H']} (∩grep {stats['HG']}, heuristic-only {stats['H_only']})")