cgraphx 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/.claude-template/skills/cgraphx/SKILL.md +3 -3
- package/dist/.claude-template/skills/cgraphx/agent-prompt.md +1 -1
- package/dist/.claude-template/skills/cgraphx-guide/SKILL.md +94 -0
- package/dist/.claude-template/skills/cgraphx-guide/how-to-use.html +403 -0
- package/dist/.claude-template/skills/clarify-requirements/SKILL.md +19 -8
- package/dist/.claude-template/skills/code-impact-docgen/SKILL.md +186 -176
- package/dist/.claude-template/skills/code-impact-docgen/template-design-html.md +357 -0
- package/dist/.claude-template/skills/code-impact-docgen/template-design-md.md +164 -0
- package/dist/.claude-template/skills/code-impact-init/SKILL.md +47 -47
- package/dist/.claude-template/skills/developer-timeline/SKILL.md +9 -0
- package/dist/.claude-template/skills/write-api-doc/SKILL.md +317 -0
- package/dist/.claude-template/skills/write-api-doc/template-api-html.md +422 -0
- package/dist/.claude-template/skills/write-plan/SKILL.md +38 -16
- package/dist/.claude-template/skills/write-prd/SKILL.md +32 -8
- package/dist/.claude-template/skills/write-spec/SKILL.md +34 -9
- package/dist/bin/codegraph.js +0 -100
- package/dist/bin/codegraph.js.map +1 -1
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +13 -0
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/scope-index.d.ts +86 -0
- package/dist/resolution/scope-index.d.ts.map +1 -0
- package/dist/resolution/scope-index.js +143 -0
- package/dist/resolution/scope-index.js.map +1 -0
- package/dist/resolution/stdlib-blocklist.d.ts +53 -0
- package/dist/resolution/stdlib-blocklist.d.ts.map +1 -0
- package/dist/resolution/stdlib-blocklist.js +143 -0
- package/dist/resolution/stdlib-blocklist.js.map +1 -0
- package/dist/search/ast-helpers.d.ts +42 -0
- package/dist/search/ast-helpers.d.ts.map +1 -0
- package/dist/search/ast-helpers.js +106 -0
- package/dist/search/ast-helpers.js.map +1 -0
- package/dist/search/call-sites.d.ts +398 -0
- package/dist/search/call-sites.d.ts.map +1 -0
- package/dist/search/call-sites.js +1433 -0
- package/dist/search/call-sites.js.map +1 -0
- package/dist/search/context.d.ts +134 -0
- package/dist/search/context.d.ts.map +1 -0
- package/dist/search/context.js +575 -0
- package/dist/search/context.js.map +1 -0
- package/dist/search/impact.d.ts +139 -0
- package/dist/search/impact.d.ts.map +1 -0
- package/dist/search/impact.js +646 -0
- package/dist/search/impact.js.map +1 -0
- package/dist/search/related.d.ts +178 -0
- package/dist/search/related.d.ts.map +1 -0
- package/dist/search/related.js +667 -0
- package/dist/search/related.js.map +1 -0
- package/dist/search/slice.d.ts +148 -0
- package/dist/search/slice.d.ts.map +1 -0
- package/dist/search/slice.js +460 -0
- package/dist/search/slice.js.map +1 -0
- package/dist/search/snr-constants.d.ts +41 -0
- package/dist/search/snr-constants.d.ts.map +1 -0
- package/dist/search/snr-constants.js +44 -0
- package/dist/search/snr-constants.js.map +1 -0
- package/dist/search/types.d.ts +28 -0
- package/dist/search/types.d.ts.map +1 -0
- package/dist/search/types.js +12 -0
- package/dist/search/types.js.map +1 -0
- package/dist/timeline/cli.d.ts.map +1 -1
- package/dist/timeline/cli.js +22 -3
- package/dist/timeline/cli.js.map +1 -1
- package/dist/timeline/store.d.ts +5 -0
- package/dist/timeline/store.d.ts.map +1 -1
- package/dist/timeline/store.js +23 -3
- package/dist/timeline/store.js.map +1 -1
- package/package.json +1 -1
- package/scripts/agent-eval/block-cgraphx-and-gitnexus-cli-hook.sh +43 -0
- package/scripts/agent-eval/block-cgraphx-cli-hook.sh +32 -0
- package/scripts/agent-eval/block-cgraphx-cli-settings.json +16 -0
- package/scripts/agent-eval/cli-vs-mcp-3arm.sh +121 -0
- package/scripts/agent-eval/multi-tool-eval.sh +171 -0
- package/scripts/agent-eval/parse-cli-vs-mcp.mjs +232 -0
- package/scripts/agent-eval/parse-multi-tool.mjs +242 -0
- package/scripts/agent-eval/subagent-token-cost.py +188 -0
- package/dist/.claude-template/skills/code-impact-docgen/template-business-html.md +0 -242
- package/dist/.claude-template/skills/code-impact-docgen/template-business-md.md +0 -107
- package/dist/.claude-template/skills/code-impact-docgen/template-technical-html.md +0 -205
- package/dist/.claude-template/skills/code-impact-docgen/template-technical-md.md +0 -155
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Impact analysis — transitive callers + type references for a method.
|
|
4
|
+
*
|
|
5
|
+
* Spec input (per `docs/features/2026-07-01-impact-command/spec.md`, drafted
|
|
6
|
+
* from clarify-requirements):
|
|
7
|
+
* - Replace the existing indexed-DB `cgraphx impact` command (CLI surface only;
|
|
8
|
+
* the graph API `CodeGraph.getImpactRadius` stays for the MCP server until
|
|
9
|
+
* that's separately deprecated).
|
|
10
|
+
* - Input: `Class#method` or `file:line` (same as `calls --from`).
|
|
11
|
+
* - Output: transitive callers (BFS up to depth N) + type references to the
|
|
12
|
+
* enclosing class (import / extends / implements / field / param / local /
|
|
13
|
+
* cast / annotation / generic). All SNR-ranked, tiered-budget output.
|
|
14
|
+
*
|
|
15
|
+
* Isolation: lives entirely under `src/search/`, imports nothing from
|
|
16
|
+
* `src/graph/`, `src/resolution/`, `src/db/`, `src/mcp/`, or `src/extraction/`
|
|
17
|
+
* (beyond the grammar loader). When the indexed pipeline is eventually removed,
|
|
18
|
+
* this file is untouched.
|
|
19
|
+
*/
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.searchCallSites = void 0;
|
|
55
|
+
exports.computeImpactCallSiteSnr = computeImpactCallSiteSnr;
|
|
56
|
+
exports.computeImpactSnrBreakdown = computeImpactSnrBreakdown;
|
|
57
|
+
exports.searchImpact = searchImpact;
|
|
58
|
+
exports.applyImpactTieredBudget = applyImpactTieredBudget;
|
|
59
|
+
exports.formatImpactResult = formatImpactResult;
|
|
60
|
+
const fs_1 = require("fs");
|
|
61
|
+
const path = __importStar(require("path"));
|
|
62
|
+
const grammars_1 = require("../extraction/grammars");
|
|
63
|
+
const call_sites_1 = require("./call-sites");
|
|
64
|
+
const ast_helpers_1 = require("./ast-helpers");
|
|
65
|
+
// Re-export for callers-mode reuse
|
|
66
|
+
var call_sites_2 = require("./call-sites");
|
|
67
|
+
Object.defineProperty(exports, "searchCallSites", { enumerable: true, get: function () { return call_sites_2.searchCallSites; } });
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// SNR scoring for type references
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
/**
|
|
72
|
+
* SNR score a type reference. The signal here is "how directly does this
|
|
73
|
+
* reference propagate a change to the method".
|
|
74
|
+
*
|
|
75
|
+
* extends/implements → +50 (subclass MUST implement/inherit the method)
|
|
76
|
+
* field → +35 (field is reached via the method's class)
|
|
77
|
+
* param/local/return → +25 (typed usage; method change might affect callers)
|
|
78
|
+
* annotation → +20 (annotation processor might break)
|
|
79
|
+
* cast → +15 (runtime cast; weakly affected)
|
|
80
|
+
* generic → +10 (e.g. List<Foo>; least affected by method change)
|
|
81
|
+
* import → +5 (might be unused or fully unrelated)
|
|
82
|
+
* other → +10
|
|
83
|
+
*/
|
|
84
|
+
function computeTypeRefSnr(ref) {
|
|
85
|
+
switch (ref.kind) {
|
|
86
|
+
case 'extends': return 50;
|
|
87
|
+
case 'implements': return 50;
|
|
88
|
+
case 'field': return 35;
|
|
89
|
+
case 'param': return 25;
|
|
90
|
+
case 'local': return 25;
|
|
91
|
+
case 'return': return 25;
|
|
92
|
+
case 'annotation': return 20;
|
|
93
|
+
case 'cast': return 15;
|
|
94
|
+
case 'generic': return 10;
|
|
95
|
+
case 'import': return 5;
|
|
96
|
+
case 'other': return 10;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* SNR score a transitive caller. Reuses computeSnrScore for the call-site
|
|
101
|
+
* itself, then applies hop-distance decay (hop 1 = 1.0, hop 2 = 0.6,
|
|
102
|
+
* hop 3 = 0.36, ...). Direct callers always outrank indirect ones.
|
|
103
|
+
*/
|
|
104
|
+
function computeImpactCallSiteSnr(site) {
|
|
105
|
+
const base = (0, call_sites_1.computeSnrScore)(site);
|
|
106
|
+
const decay = Math.pow(0.6, site.hopDistance - 1);
|
|
107
|
+
return Math.round(base * decay);
|
|
108
|
+
}
|
|
109
|
+
function computeImpactSnrBreakdown(site) {
|
|
110
|
+
const baseBd = (0, call_sites_1.computeSnrBreakdown)(site);
|
|
111
|
+
const decayMultiplier = Math.pow(0.6, site.hopDistance - 1);
|
|
112
|
+
return {
|
|
113
|
+
base: baseBd.total,
|
|
114
|
+
decayMultiplier,
|
|
115
|
+
total: Math.round(baseBd.total * decayMultiplier),
|
|
116
|
+
matchedRules: baseBd.matchedRules,
|
|
117
|
+
receiver: baseBd.receiver,
|
|
118
|
+
method: baseBd.method,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Format an impact SNR breakdown for the `--explain` flag. Hop 1 (decay 1.0)
|
|
123
|
+
* collapses to the plain breakdown; deeper hops show `base × decay = total`.
|
|
124
|
+
*/
|
|
125
|
+
function formatImpactSnrBreakdown(site) {
|
|
126
|
+
const bd = computeImpactSnrBreakdown(site);
|
|
127
|
+
// At hop 1 (decay 1.0) the base equals the total, so the multiplier is
|
|
128
|
+
// noise — show the plain call-site breakdown instead.
|
|
129
|
+
if (site.hopDistance === 1) {
|
|
130
|
+
return (0, call_sites_1.formatSnrBreakdown)(site);
|
|
131
|
+
}
|
|
132
|
+
// Deeper hops: strip the trailing `= <total>` from the call-site breakdown
|
|
133
|
+
// (that's the pre-decay base) and append the hop multiplier.
|
|
134
|
+
const baseFull = (0, call_sites_1.formatSnrBreakdown)(site);
|
|
135
|
+
const baseStr = baseFull.replace(/\s*=\s*-?\d+$/, '').trim();
|
|
136
|
+
const decayPct = `${Math.round(bd.decayMultiplier * 100)}%`;
|
|
137
|
+
return `${baseStr} × ${decayPct} hop${site.hopDistance} = ${bd.total}`;
|
|
138
|
+
}
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Main entry
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
async function searchImpact(projectRoot, symbol, options = {}) {
|
|
143
|
+
const { depth = 2, topPerHop = 20, topTypeRefs = 15, budget = 6000, languages = ['java', 'kotlin'], excludeDirs = [], } = options;
|
|
144
|
+
await (0, grammars_1.loadGrammarsForLanguages)(languages);
|
|
145
|
+
const excludeSet = new Set([...call_sites_1.DEFAULT_EXCLUDE_DIRS, ...excludeDirs]);
|
|
146
|
+
// 1. Resolve symbol → method_declaration AST node + enclosing class name
|
|
147
|
+
const resolved = await (0, call_sites_1.resolveMethodSymbol)(projectRoot, symbol, languages);
|
|
148
|
+
const classNode = (0, call_sites_1.findEnclosingClassType)(resolved.methodNode);
|
|
149
|
+
const enclosingClass = classNode?.childForFieldName('name')?.text ??
|
|
150
|
+
resolved.methodQualifiedName.split('#')[0] ??
|
|
151
|
+
null;
|
|
152
|
+
// 2. BFS transitive callers
|
|
153
|
+
const { callers, depthReached } = await findTransitiveCallers(projectRoot, {
|
|
154
|
+
methodName: resolved.methodNode.childForFieldName('name')?.text ?? '',
|
|
155
|
+
className: enclosingClass ?? '',
|
|
156
|
+
filePath: resolved.file,
|
|
157
|
+
methodNode: resolved.methodNode,
|
|
158
|
+
}, depth, { topPerHop, languages, excludeDirs: excludeSet });
|
|
159
|
+
// 3. Type references scan (only if we resolved an enclosing class)
|
|
160
|
+
const typeReferences = enclosingClass
|
|
161
|
+
? await scanTypeReferences(projectRoot, enclosingClass, languages, excludeSet)
|
|
162
|
+
: [];
|
|
163
|
+
// 4. SNR scoring
|
|
164
|
+
for (const c of callers) {
|
|
165
|
+
c.snrScore = computeImpactCallSiteSnr(c);
|
|
166
|
+
}
|
|
167
|
+
for (const r of typeReferences) {
|
|
168
|
+
r.snrScore = computeTypeRefSnr(r);
|
|
169
|
+
}
|
|
170
|
+
// 5. Sort + top-K
|
|
171
|
+
callers.sort(compareByImpactSnr);
|
|
172
|
+
const callersKept = callers.slice(0, topPerHop * Math.max(depthReached, 1));
|
|
173
|
+
const callersPrunedByTopPerHop = callers.length - callersKept.length;
|
|
174
|
+
typeReferences.sort((a, b) => b.snrScore - a.snrScore || a.line - b.line);
|
|
175
|
+
const typeRefsKept = typeReferences.slice(0, topTypeRefs);
|
|
176
|
+
const typeRefsPrunedByTopK = typeReferences.length - typeRefsKept.length;
|
|
177
|
+
// 6. Tiered budget — two-list allocation: callers first, then type refs
|
|
178
|
+
const { tieredCallers, tieredTypeRefs, truncatedByBudget } = applyImpactTieredBudget(callersKept, typeRefsKept, budget, { forceRecommended: options.withSnippet ?? false, forceOptional: options.full ?? false });
|
|
179
|
+
return {
|
|
180
|
+
symbol,
|
|
181
|
+
methodQualifiedName: resolved.methodQualifiedName,
|
|
182
|
+
enclosingClass,
|
|
183
|
+
filePath: path.relative(projectRoot, resolved.file).replace(/\\/g, '/'),
|
|
184
|
+
startLine: resolved.methodNode.startPosition.row + 1,
|
|
185
|
+
endLine: resolved.methodNode.endPosition.row + 1,
|
|
186
|
+
depthReached,
|
|
187
|
+
totalCallers: callers.length,
|
|
188
|
+
totalTypeRefFiles: new Set(typeReferences.map((r) => r.filePath)).size,
|
|
189
|
+
totalTypeRefs: typeReferences.length,
|
|
190
|
+
callers: tieredCallers,
|
|
191
|
+
typeReferences: tieredTypeRefs,
|
|
192
|
+
omitted: {
|
|
193
|
+
callersPrunedByTopPerHop,
|
|
194
|
+
typeRefsPrunedByTopK,
|
|
195
|
+
truncatedByBudget,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* BFS over callers. For each method, find all callsites via `searchCallSites`
|
|
201
|
+
* (calls callers mode), then recurse on each unique containing method.
|
|
202
|
+
*
|
|
203
|
+
* Cycle detection: a visited Set<string> keyed `Class#method`. If a caller
|
|
204
|
+
* is already visited, skip — prevents infinite loops on recursion (A calls B,
|
|
205
|
+
* B calls A).
|
|
206
|
+
*
|
|
207
|
+
* The hop-level topK (`topPerHop`) caps how many callers we surface per hop
|
|
208
|
+
* to bound the output. Lower-signal callers (noise) get pruned.
|
|
209
|
+
*/
|
|
210
|
+
async function findTransitiveCallers(projectRoot, start, maxDepth, options) {
|
|
211
|
+
// Lazy import — searchCallSites pulls grammars, which we already loaded
|
|
212
|
+
const { searchCallSites } = await Promise.resolve().then(() => __importStar(require('./call-sites')));
|
|
213
|
+
const visited = new Set();
|
|
214
|
+
const visitedCallsites = new Set(); // dedup by `${filePath}:${line}`
|
|
215
|
+
const startKey = `${start.className}#${start.methodName}`;
|
|
216
|
+
visited.add(startKey);
|
|
217
|
+
// Queue carries className + methodName so we can filter searchCallSites
|
|
218
|
+
// results to only those whose containingClass matches — without this filter,
|
|
219
|
+
// common method names like `create` / `get` / `process` pull in hundreds of
|
|
220
|
+
// unrelated callsites and drown the BFS in noise (the method-name-ambiguity
|
|
221
|
+
// problem).
|
|
222
|
+
const queue = [
|
|
223
|
+
{ methodName: start.methodName, expectedClass: start.className || null, hop: 0 },
|
|
224
|
+
];
|
|
225
|
+
const allCallers = [];
|
|
226
|
+
let depthReached = 0;
|
|
227
|
+
while (queue.length > 0) {
|
|
228
|
+
const { methodName, expectedClass, hop } = queue.shift();
|
|
229
|
+
if (hop >= maxDepth)
|
|
230
|
+
continue;
|
|
231
|
+
depthReached = Math.max(depthReached, hop + 1);
|
|
232
|
+
// Find callers of methodName across the project
|
|
233
|
+
const result = await searchCallSites(projectRoot, methodName, {
|
|
234
|
+
languages: options.languages,
|
|
235
|
+
excludeDirs: [...options.excludeDirs],
|
|
236
|
+
topK: options.topPerHop * 2, // pull more, we'll filter + dedupe
|
|
237
|
+
});
|
|
238
|
+
// Filter to callsites whose containingClass matches what we expect — BUT
|
|
239
|
+
// only from hop 2 onwards. At hop 1 we want ALL callers of the start
|
|
240
|
+
// method (the start method's class doesn't constrain who calls it).
|
|
241
|
+
// From hop 2: each queued entry's expectedClass is the prior-hop caller's
|
|
242
|
+
// containingClass, so filtering keeps only callsites that live INSIDE
|
|
243
|
+
// that class — i.e., callers of that specific Class#method, not every
|
|
244
|
+
// method of the same name across the project. Without this, common names
|
|
245
|
+
// (`create`/`get`/`process`) pull in hundreds of unrelated callsites.
|
|
246
|
+
const filtered = (hop >= 1 && expectedClass)
|
|
247
|
+
? result.callSites.filter((s) => s.containingClass === expectedClass)
|
|
248
|
+
: result.callSites;
|
|
249
|
+
// Pull top-K per hop. Dedupe by (filePath, line) across ALL hops — BFS
|
|
250
|
+
// can reach the same callsite via multiple paths (A→B→C and A→D→C both
|
|
251
|
+
// land on C), but we only want to surface C once at its shallowest hop.
|
|
252
|
+
// Also skip self-calls (callsite inside a method we've already visited
|
|
253
|
+
// from) — these are the method calling its own name on a different
|
|
254
|
+
// receiver (e.g., `orderClient.sceneCommit()` inside
|
|
255
|
+
// `CustomerOrderServiceImpl#sceneCommit`), not a real caller.
|
|
256
|
+
const takenThisHop = filtered.slice(0, options.topPerHop);
|
|
257
|
+
for (const site of takenThisHop) {
|
|
258
|
+
const callsiteKey = `${site.filePath}:${site.line}`;
|
|
259
|
+
if (visitedCallsites.has(callsiteKey))
|
|
260
|
+
continue;
|
|
261
|
+
const callerKey = site.containingClass && site.containingMethod
|
|
262
|
+
? `${site.containingClass}#${site.containingMethod}`
|
|
263
|
+
: null;
|
|
264
|
+
if (callerKey && visited.has(callerKey))
|
|
265
|
+
continue; // self-call or already-covered
|
|
266
|
+
visitedCallsites.add(callsiteKey);
|
|
267
|
+
const impactSite = {
|
|
268
|
+
...site,
|
|
269
|
+
hopDistance: hop + 1,
|
|
270
|
+
};
|
|
271
|
+
allCallers.push(impactSite);
|
|
272
|
+
// Recurse on each unique containing method (Class#method)
|
|
273
|
+
if (callerKey) {
|
|
274
|
+
visited.add(callerKey);
|
|
275
|
+
queue.push({
|
|
276
|
+
methodName: site.containingMethod,
|
|
277
|
+
expectedClass: site.containingClass,
|
|
278
|
+
hop: hop + 1,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { callers: allCallers, depthReached };
|
|
284
|
+
}
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Type references scan
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
/**
|
|
289
|
+
* Walk all source files looking for type_identifier nodes matching `className`.
|
|
290
|
+
* For each match, classify the kind (extends/implements/field/param/...) by
|
|
291
|
+
* looking at the AST parent chain.
|
|
292
|
+
*
|
|
293
|
+
* Fast text pre-filter: skip files that don't contain `className` as text.
|
|
294
|
+
*/
|
|
295
|
+
async function scanTypeReferences(projectRoot, className, languages, excludeDirs) {
|
|
296
|
+
const refs = [];
|
|
297
|
+
const files = await (0, call_sites_1.listJavaFamilyFiles)(projectRoot, excludeDirs);
|
|
298
|
+
for (const file of files) {
|
|
299
|
+
let content;
|
|
300
|
+
try {
|
|
301
|
+
content = await fs_1.promises.readFile(file, 'utf-8');
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
// Fast text filter — most files won't mention this class
|
|
307
|
+
if (!content.includes(className))
|
|
308
|
+
continue;
|
|
309
|
+
const language = (0, grammars_1.detectLanguage)(file, content);
|
|
310
|
+
if (!languages.includes(language))
|
|
311
|
+
continue;
|
|
312
|
+
const parser = (0, grammars_1.getParser)(language);
|
|
313
|
+
if (!parser)
|
|
314
|
+
continue;
|
|
315
|
+
const tree = parser.parse(content);
|
|
316
|
+
if (!tree)
|
|
317
|
+
continue;
|
|
318
|
+
const relPath = path.relative(projectRoot, file).replace(/\\/g, '/');
|
|
319
|
+
(0, ast_helpers_1.walkTree)(tree.rootNode, (node) => {
|
|
320
|
+
// Java/Kotlin: type_identifier is the typical class-name node.
|
|
321
|
+
// Kotlin also uses 'user_type_name' in some contexts.
|
|
322
|
+
if (node.type !== 'type_identifier' && node.type !== 'user_type_name')
|
|
323
|
+
return;
|
|
324
|
+
if (node.text !== className)
|
|
325
|
+
return;
|
|
326
|
+
const kind = classifyTypeReference(node);
|
|
327
|
+
// For import statements, the type_identifier might be the LAST segment of
|
|
328
|
+
// a scoped_type_identifier — avoid double-counting by checking we're the
|
|
329
|
+
// last identifier in our parent chain.
|
|
330
|
+
if (kind === 'import' && isInnerSegmentOfScopedType(node))
|
|
331
|
+
return;
|
|
332
|
+
const { containingClass, containingMethod } = findEnclosingContext(node);
|
|
333
|
+
refs.push({
|
|
334
|
+
className,
|
|
335
|
+
filePath: relPath,
|
|
336
|
+
line: node.startPosition.row + 1,
|
|
337
|
+
column: node.startPosition.column,
|
|
338
|
+
kind,
|
|
339
|
+
containingClass,
|
|
340
|
+
containingMethod,
|
|
341
|
+
snippet: extractOneLine(content, node.startPosition.row),
|
|
342
|
+
snrScore: 0, // scored later
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return refs;
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Classify a type_identifier reference by walking its parent chain.
|
|
350
|
+
*
|
|
351
|
+
* Tree-sitter-java AST shapes (verified empirically):
|
|
352
|
+
* import com.x.Foo; — import_declaration > scoped_type_identifier > type_identifier
|
|
353
|
+
* class Bar extends Foo — class_declaration > super_class > type_identifier
|
|
354
|
+
* class Bar implements F — class_declaration > implements > interface_type_list > type_identifier
|
|
355
|
+
* private Foo foo; — field_declaration > type > type_identifier
|
|
356
|
+
* void m(Foo x) — formal_parameter > type > type_identifier
|
|
357
|
+
* Foo x = ...; — local_variable_declaration > type > type_identifier
|
|
358
|
+
* (Foo) obj — cast_expression > type > type_identifier
|
|
359
|
+
* List<Foo> — generic_type > type_arguments > type_identifier
|
|
360
|
+
* @Foo — annotation > type_identifier (or qualified_name)
|
|
361
|
+
* Foo m() — method_declaration > type > type_identifier (return type)
|
|
362
|
+
*/
|
|
363
|
+
function classifyTypeReference(node) {
|
|
364
|
+
// Walk up: look at parent and the closest "interesting" ancestor
|
|
365
|
+
let parent = node.parent;
|
|
366
|
+
// Direct parent checks first
|
|
367
|
+
if (parent?.type === 'annotation')
|
|
368
|
+
return 'annotation';
|
|
369
|
+
if (parent?.type === 'cast_expression')
|
|
370
|
+
return 'cast';
|
|
371
|
+
if (parent?.type === 'generic_type')
|
|
372
|
+
return 'generic';
|
|
373
|
+
// import_declaration ancestor
|
|
374
|
+
let p = node;
|
|
375
|
+
while (p) {
|
|
376
|
+
if (p.type === 'import_declaration')
|
|
377
|
+
return 'import';
|
|
378
|
+
p = p.parent;
|
|
379
|
+
}
|
|
380
|
+
// super_class / implements
|
|
381
|
+
if (parent?.type === 'super_class')
|
|
382
|
+
return 'extends';
|
|
383
|
+
if (parent?.type === 'super_interfaces' || parent?.type === 'interface_type_list')
|
|
384
|
+
return 'implements';
|
|
385
|
+
// type field of various declarations
|
|
386
|
+
if (parent?.type === 'type') {
|
|
387
|
+
const grand = parent.parent;
|
|
388
|
+
if (!grand)
|
|
389
|
+
return 'other';
|
|
390
|
+
switch (grand.type) {
|
|
391
|
+
case 'field_declaration': return 'field';
|
|
392
|
+
case 'formal_parameter': return 'param';
|
|
393
|
+
case 'local_variable_declaration':
|
|
394
|
+
case 'variable_declarator': return 'local';
|
|
395
|
+
case 'method_declaration': return 'return';
|
|
396
|
+
default: return 'other';
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return 'other';
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* True if this type_identifier is an inner segment of a scoped_type_identifier
|
|
403
|
+
* (e.g., the `http` in `com.example.http.Client`). For import deduplication —
|
|
404
|
+
* we only want the LAST type_identifier (the actual class name).
|
|
405
|
+
*/
|
|
406
|
+
function isInnerSegmentOfScopedType(node) {
|
|
407
|
+
return node.parent?.type === 'scoped_type_identifier';
|
|
408
|
+
}
|
|
409
|
+
function findEnclosingContext(node) {
|
|
410
|
+
let containingClass = null;
|
|
411
|
+
let containingMethod = null;
|
|
412
|
+
let cur = node.parent;
|
|
413
|
+
while (cur) {
|
|
414
|
+
if (!containingClass && (cur.type === 'class_declaration' ||
|
|
415
|
+
cur.type === 'interface_declaration' ||
|
|
416
|
+
cur.type === 'enum_declaration' ||
|
|
417
|
+
cur.type === 'record_declaration')) {
|
|
418
|
+
const name = cur.childForFieldName('name')?.text ?? null;
|
|
419
|
+
if (name)
|
|
420
|
+
containingClass = name;
|
|
421
|
+
}
|
|
422
|
+
if (!containingMethod && (cur.type === 'method_declaration' ||
|
|
423
|
+
cur.type === 'constructor_declaration' ||
|
|
424
|
+
cur.type === 'compact_constructor_declaration')) {
|
|
425
|
+
const name = cur.childForFieldName('name')?.text ?? null;
|
|
426
|
+
if (name)
|
|
427
|
+
containingMethod = name;
|
|
428
|
+
}
|
|
429
|
+
if (containingClass && containingMethod)
|
|
430
|
+
break;
|
|
431
|
+
cur = cur.parent;
|
|
432
|
+
}
|
|
433
|
+
return { containingClass, containingMethod };
|
|
434
|
+
}
|
|
435
|
+
function extractOneLine(content, row) {
|
|
436
|
+
const lines = content.split('\n');
|
|
437
|
+
return (lines[row] ?? '').trim();
|
|
438
|
+
}
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
// Sort + tiered budget
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
function compareByImpactSnr(a, b) {
|
|
443
|
+
const sa = a.snrScore ?? 0;
|
|
444
|
+
const sb = b.snrScore ?? 0;
|
|
445
|
+
if (sa !== sb)
|
|
446
|
+
return sb - sa;
|
|
447
|
+
// Lower hop distance first on tie (direct caller before indirect)
|
|
448
|
+
if (a.hopDistance !== b.hopDistance)
|
|
449
|
+
return a.hopDistance - b.hopDistance;
|
|
450
|
+
const da = a.filePath.split('/').length;
|
|
451
|
+
const db = b.filePath.split('/').length;
|
|
452
|
+
if (da !== db)
|
|
453
|
+
return da - db;
|
|
454
|
+
if (a.filePath !== b.filePath)
|
|
455
|
+
return a.filePath.localeCompare(b.filePath);
|
|
456
|
+
return a.line - b.line;
|
|
457
|
+
}
|
|
458
|
+
const TIER_ESSENTIAL_COST = 40;
|
|
459
|
+
const TIER_RECOMMENDED_ADDITIONAL = 120;
|
|
460
|
+
const TIER_OPTIONAL_ADDITIONAL = 400;
|
|
461
|
+
/**
|
|
462
|
+
* Two-list tiered budget: allocate budget across callers AND type references.
|
|
463
|
+
* Callers get essential tier first (in SNR order), then type refs get essential
|
|
464
|
+
* tier. If budget remains, recommended tier fills in SNR order across both
|
|
465
|
+
* lists. Same for optional.
|
|
466
|
+
*/
|
|
467
|
+
function applyImpactTieredBudget(callers, typeRefs, budget, opts = {}) {
|
|
468
|
+
// Pass 1: how many items fit at essential tier across both lists?
|
|
469
|
+
let count = 0;
|
|
470
|
+
let used = 0;
|
|
471
|
+
const total = callers.length + typeRefs.length;
|
|
472
|
+
for (let i = 0; i < total; i++) {
|
|
473
|
+
if (used + TIER_ESSENTIAL_COST > budget && count > 0)
|
|
474
|
+
break;
|
|
475
|
+
used += TIER_ESSENTIAL_COST;
|
|
476
|
+
count++;
|
|
477
|
+
}
|
|
478
|
+
const truncatedByBudget = total - count;
|
|
479
|
+
// Allocate essential slots in priority order: callers first (in SNR order),
|
|
480
|
+
// then type refs (in SNR order)
|
|
481
|
+
const callerCount = Math.min(callers.length, count);
|
|
482
|
+
const typeRefCount = Math.min(typeRefs.length, count - callerCount);
|
|
483
|
+
const tieredCallers = callers.slice(0, callerCount).map((c) => ({ ...c, tier: 'essential' }));
|
|
484
|
+
const tieredTypeRefs = typeRefs.slice(0, typeRefCount).map((r) => ({ ...r, tier: 'essential' }));
|
|
485
|
+
// Force flags short-circuit
|
|
486
|
+
if (opts.forceOptional) {
|
|
487
|
+
for (const c of tieredCallers)
|
|
488
|
+
c.tier = 'optional';
|
|
489
|
+
for (const r of tieredTypeRefs)
|
|
490
|
+
r.tier = 'optional';
|
|
491
|
+
return { tieredCallers, tieredTypeRefs, truncatedByBudget };
|
|
492
|
+
}
|
|
493
|
+
if (opts.forceRecommended) {
|
|
494
|
+
for (const c of tieredCallers)
|
|
495
|
+
c.tier = 'recommended';
|
|
496
|
+
for (const r of tieredTypeRefs)
|
|
497
|
+
r.tier = 'recommended';
|
|
498
|
+
return { tieredCallers, tieredTypeRefs, truncatedByBudget };
|
|
499
|
+
}
|
|
500
|
+
// Pass 2: upgrade to recommended in SNR order, callers first then type refs
|
|
501
|
+
let remaining = budget - used;
|
|
502
|
+
for (const c of tieredCallers) {
|
|
503
|
+
if (remaining >= TIER_RECOMMENDED_ADDITIONAL) {
|
|
504
|
+
c.tier = 'recommended';
|
|
505
|
+
remaining -= TIER_RECOMMENDED_ADDITIONAL;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
for (const r of tieredTypeRefs) {
|
|
509
|
+
if (remaining >= TIER_RECOMMENDED_ADDITIONAL) {
|
|
510
|
+
r.tier = 'recommended';
|
|
511
|
+
remaining -= TIER_RECOMMENDED_ADDITIONAL;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// Pass 3: upgrade to optional
|
|
515
|
+
for (const c of tieredCallers) {
|
|
516
|
+
if (c.tier !== 'recommended')
|
|
517
|
+
continue;
|
|
518
|
+
if (remaining >= TIER_OPTIONAL_ADDITIONAL) {
|
|
519
|
+
c.tier = 'optional';
|
|
520
|
+
remaining -= TIER_OPTIONAL_ADDITIONAL;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const r of tieredTypeRefs) {
|
|
524
|
+
if (r.tier !== 'recommended')
|
|
525
|
+
continue;
|
|
526
|
+
if (remaining >= TIER_OPTIONAL_ADDITIONAL) {
|
|
527
|
+
r.tier = 'optional';
|
|
528
|
+
remaining -= TIER_OPTIONAL_ADDITIONAL;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return { tieredCallers, tieredTypeRefs, truncatedByBudget };
|
|
532
|
+
}
|
|
533
|
+
// ---------------------------------------------------------------------------
|
|
534
|
+
// Formatter (CLI markdown)
|
|
535
|
+
// ---------------------------------------------------------------------------
|
|
536
|
+
function formatImpactResult(result, options) {
|
|
537
|
+
const lines = [];
|
|
538
|
+
lines.push(`**Impact of \`${result.methodQualifiedName}\`** \`${result.filePath}:${result.startLine}-${result.endLine}\``);
|
|
539
|
+
if (result.totalCallers === 0 && result.totalTypeRefs === 0) {
|
|
540
|
+
lines.push('');
|
|
541
|
+
lines.push(`_No callers and no type references found — method appears isolated (entry point or unused)._`);
|
|
542
|
+
return lines.join('\n');
|
|
543
|
+
}
|
|
544
|
+
lines.push(`— ${result.totalCallers} caller${result.totalCallers === 1 ? '' : 's'} (BFS depth ${result.depthReached}) · ${result.totalTypeRefs} type ref${result.totalTypeRefs === 1 ? '' : 's'} to \`${result.enclosingClass ?? '(unknown class)'}\` across ${result.totalTypeRefFiles} file${result.totalTypeRefFiles === 1 ? '' : 's'}`);
|
|
545
|
+
// --- Callers section (grouped by hop) ---
|
|
546
|
+
if (result.callers.length > 0) {
|
|
547
|
+
lines.push('');
|
|
548
|
+
lines.push(`## Callers (transitive, BFS)`);
|
|
549
|
+
// Group by hop, then SNR within hop
|
|
550
|
+
const byHop = new Map();
|
|
551
|
+
for (const c of result.callers) {
|
|
552
|
+
const group = byHop.get(c.hopDistance) ?? [];
|
|
553
|
+
group.push(c);
|
|
554
|
+
byHop.set(c.hopDistance, group);
|
|
555
|
+
}
|
|
556
|
+
const hops = [...byHop.keys()].sort((a, b) => a - b);
|
|
557
|
+
for (const hop of hops) {
|
|
558
|
+
const group = byHop.get(hop).sort(compareByImpactSnr);
|
|
559
|
+
lines.push('');
|
|
560
|
+
lines.push(`### Hop ${hop}${hop === 1 ? ' — direct callers' : ` — indirect (callers of callers, depth ${hop})`}`);
|
|
561
|
+
for (const c of group) {
|
|
562
|
+
const tag = c.crossBoundary
|
|
563
|
+
? ` **[xBdy:${c.crossBoundary.fieldType}]**`
|
|
564
|
+
: c.sameClassBiz
|
|
565
|
+
? ' **[sibling]**'
|
|
566
|
+
: '';
|
|
567
|
+
lines.push(`- **${c.methodName}** L${c.line} \`${c.filePath}:${c.line}\`${tag}`);
|
|
568
|
+
if (c.containingClass && c.containingMethod) {
|
|
569
|
+
lines.push(` _in ${c.containingClass}#${c.containingMethod}_`);
|
|
570
|
+
}
|
|
571
|
+
if (c.tier === 'recommended' || c.tier === 'optional') {
|
|
572
|
+
if (c.snippet) {
|
|
573
|
+
const snippetLines = c.snippet.split('\n').slice(0, 2);
|
|
574
|
+
for (const sl of snippetLines) {
|
|
575
|
+
lines.push(` > ${sl}`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (options?.explain) {
|
|
580
|
+
lines.push(` _score: ${formatImpactSnrBreakdown(c)}_`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
lines.push('');
|
|
587
|
+
lines.push(`## Callers — none (likely entry point or RMI target)`);
|
|
588
|
+
}
|
|
589
|
+
// --- Type references section ---
|
|
590
|
+
if (result.typeReferences.length > 0) {
|
|
591
|
+
lines.push('');
|
|
592
|
+
lines.push(`## Type references to \`${result.enclosingClass}\` (ranked)`);
|
|
593
|
+
let currentKind = null;
|
|
594
|
+
for (const r of result.typeReferences) {
|
|
595
|
+
if (r.kind !== currentKind) {
|
|
596
|
+
currentKind = r.kind;
|
|
597
|
+
lines.push('');
|
|
598
|
+
lines.push(`### ${kindLabel(r.kind)}`);
|
|
599
|
+
}
|
|
600
|
+
const ctx = r.containingClass
|
|
601
|
+
? ` _in ${r.containingClass}${r.containingMethod ? `#${r.containingMethod}` : ''}_`
|
|
602
|
+
: r.containingMethod
|
|
603
|
+
? ` _in #${r.containingMethod}_`
|
|
604
|
+
: '';
|
|
605
|
+
lines.push(`- \`${r.filePath}:${r.line}\`${ctx}`);
|
|
606
|
+
if (r.tier === 'recommended' || r.tier === 'optional') {
|
|
607
|
+
if (r.snippet)
|
|
608
|
+
lines.push(` > ${r.snippet}`);
|
|
609
|
+
}
|
|
610
|
+
if (options?.explain) {
|
|
611
|
+
lines.push(` _score: +${r.snrScore} ${r.kind} = ${r.snrScore}_`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// Omitted summary
|
|
616
|
+
const o = result.omitted;
|
|
617
|
+
const omittedParts = [];
|
|
618
|
+
if (o.callersPrunedByTopPerHop > 0)
|
|
619
|
+
omittedParts.push(`${o.callersPrunedByTopPerHop} callers past topPerHop`);
|
|
620
|
+
if (o.typeRefsPrunedByTopK > 0)
|
|
621
|
+
omittedParts.push(`${o.typeRefsPrunedByTopK} type refs past topK`);
|
|
622
|
+
if (o.truncatedByBudget > 0)
|
|
623
|
+
omittedParts.push(`${o.truncatedByBudget} truncated by budget`);
|
|
624
|
+
if (omittedParts.length > 0) {
|
|
625
|
+
const widenHint = o.callersPrunedByTopPerHop > 0 ? '. Use `-k N` to widen' : '';
|
|
626
|
+
lines.push('');
|
|
627
|
+
lines.push(`_Omitted: ${omittedParts.join(' · ')}${widenHint}_`);
|
|
628
|
+
}
|
|
629
|
+
return lines.join('\n');
|
|
630
|
+
}
|
|
631
|
+
function kindLabel(kind) {
|
|
632
|
+
switch (kind) {
|
|
633
|
+
case 'extends': return 'extends (subclass)';
|
|
634
|
+
case 'implements': return 'implements (interface impl)';
|
|
635
|
+
case 'field': return 'field declarations';
|
|
636
|
+
case 'param': return 'method parameters';
|
|
637
|
+
case 'local': return 'local variables';
|
|
638
|
+
case 'return': return 'method return types';
|
|
639
|
+
case 'cast': return 'cast expressions';
|
|
640
|
+
case 'annotation': return 'annotations';
|
|
641
|
+
case 'generic': return 'generic type arguments';
|
|
642
|
+
case 'import': return 'imports';
|
|
643
|
+
case 'other': return 'other';
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
//# sourceMappingURL=impact.js.map
|