cgraphx 1.1.0 → 1.3.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/hooks/precommit-check/precommit-check.cjs +90 -0
- 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 +424 -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/precommit-review/SKILL.md +50 -0
- package/dist/.claude-template/skills/run-api-test/SKILL.md +187 -0
- package/dist/.claude-template/skills/run-api-test/assets/template-test-report.md +103 -0
- package/dist/.claude-template/skills/run-api-test/assets/template-test-verify.jsonl +5 -0
- package/dist/.claude-template/skills/run-api-test/references/bru-run.md +60 -0
- package/dist/.claude-template/skills/run-api-test/references/db-verification.md +81 -0
- package/dist/.claude-template/skills/run-api-test/references/report-format.md +104 -0
- package/dist/.claude-template/skills/run-api-test/references/service-readiness.md +61 -0
- package/dist/.claude-template/skills/run-api-test/references/test-scope.md +64 -0
- package/dist/.claude-template/skills/write-api/SKILL.md +150 -0
- package/dist/.claude-template/skills/write-api/assets/template-api-spec.md +112 -0
- package/dist/.claude-template/skills/write-api/assets/template-request.bru +75 -0
- package/dist/.claude-template/skills/write-api/references/ai-prompts.md +133 -0
- package/dist/.claude-template/skills/write-api/references/api-spec-format.md +108 -0
- package/dist/.claude-template/skills/write-api/references/bru-format.md +144 -0
- package/dist/.claude-template/skills/write-api/references/collection-layout.md +81 -0
- package/dist/.claude-template/skills/write-api/references/environment-setup.md +105 -0
- package/dist/.claude-template/skills/write-api/references/interface-scope.md +74 -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/api-test/ai-fields.d.ts +37 -0
- package/dist/api-test/ai-fields.d.ts.map +1 -0
- package/dist/api-test/ai-fields.js +114 -0
- package/dist/api-test/ai-fields.js.map +1 -0
- package/dist/api-test/assemble.d.ts +76 -0
- package/dist/api-test/assemble.d.ts.map +1 -0
- package/dist/api-test/assemble.js +185 -0
- package/dist/api-test/assemble.js.map +1 -0
- package/dist/api-test/bru-cli-invoker.d.ts +72 -0
- package/dist/api-test/bru-cli-invoker.d.ts.map +1 -0
- package/dist/api-test/bru-cli-invoker.js +169 -0
- package/dist/api-test/bru-cli-invoker.js.map +1 -0
- package/dist/api-test/bru-report-parser.d.ts +24 -0
- package/dist/api-test/bru-report-parser.d.ts.map +1 -0
- package/dist/api-test/bru-report-parser.js +110 -0
- package/dist/api-test/bru-report-parser.js.map +1 -0
- package/dist/api-test/bru-runner.d.ts +101 -0
- package/dist/api-test/bru-runner.d.ts.map +1 -0
- package/dist/api-test/bru-runner.js +316 -0
- package/dist/api-test/bru-runner.js.map +1 -0
- package/dist/api-test/bru-writer.d.ts +52 -0
- package/dist/api-test/bru-writer.d.ts.map +1 -0
- package/dist/api-test/bru-writer.js +159 -0
- package/dist/api-test/bru-writer.js.map +1 -0
- package/dist/api-test/call-chain-extractor.d.ts +80 -0
- package/dist/api-test/call-chain-extractor.d.ts.map +1 -0
- package/dist/api-test/call-chain-extractor.js +179 -0
- package/dist/api-test/call-chain-extractor.js.map +1 -0
- package/dist/api-test/cli.d.ts +133 -0
- package/dist/api-test/cli.d.ts.map +1 -0
- package/dist/api-test/cli.js +1009 -0
- package/dist/api-test/cli.js.map +1 -0
- package/dist/api-test/config.d.ts +75 -0
- package/dist/api-test/config.d.ts.map +1 -0
- package/dist/api-test/config.js +406 -0
- package/dist/api-test/config.js.map +1 -0
- package/dist/api-test/db-query-cli.d.ts +51 -0
- package/dist/api-test/db-query-cli.d.ts.map +1 -0
- package/dist/api-test/db-query-cli.js +119 -0
- package/dist/api-test/db-query-cli.js.map +1 -0
- package/dist/api-test/enhance-prepare.d.ts +111 -0
- package/dist/api-test/enhance-prepare.d.ts.map +1 -0
- package/dist/api-test/enhance-prepare.js +425 -0
- package/dist/api-test/enhance-prepare.js.map +1 -0
- package/dist/api-test/enhance-write.d.ts +28 -0
- package/dist/api-test/enhance-write.d.ts.map +1 -0
- package/dist/api-test/enhance-write.js +145 -0
- package/dist/api-test/enhance-write.js.map +1 -0
- package/dist/api-test/errors.d.ts +48 -0
- package/dist/api-test/errors.d.ts.map +1 -0
- package/dist/api-test/errors.js +76 -0
- package/dist/api-test/errors.js.map +1 -0
- package/dist/api-test/field-extractor.d.ts +98 -0
- package/dist/api-test/field-extractor.d.ts.map +1 -0
- package/dist/api-test/field-extractor.js +327 -0
- package/dist/api-test/field-extractor.js.map +1 -0
- package/dist/api-test/impl-finder.d.ts +37 -0
- package/dist/api-test/impl-finder.d.ts.map +1 -0
- package/dist/api-test/impl-finder.js +54 -0
- package/dist/api-test/impl-finder.js.map +1 -0
- package/dist/api-test/index.d.ts +41 -0
- package/dist/api-test/index.d.ts.map +1 -0
- package/dist/api-test/index.js +124 -0
- package/dist/api-test/index.js.map +1 -0
- package/dist/api-test/java-parser.d.ts +89 -0
- package/dist/api-test/java-parser.d.ts.map +1 -0
- package/dist/api-test/java-parser.js +508 -0
- package/dist/api-test/java-parser.js.map +1 -0
- package/dist/api-test/md-writer.d.ts +49 -0
- package/dist/api-test/md-writer.d.ts.map +1 -0
- package/dist/api-test/md-writer.js +202 -0
- package/dist/api-test/md-writer.js.map +1 -0
- package/dist/api-test/parser-httpservice.d.ts +91 -0
- package/dist/api-test/parser-httpservice.d.ts.map +1 -0
- package/dist/api-test/parser-httpservice.js +271 -0
- package/dist/api-test/parser-httpservice.js.map +1 -0
- package/dist/api-test/report.d.ts +188 -0
- package/dist/api-test/report.d.ts.map +1 -0
- package/dist/api-test/report.js +522 -0
- package/dist/api-test/report.js.map +1 -0
- package/dist/api-test/snapshot.d.ts +26 -0
- package/dist/api-test/snapshot.d.ts.map +1 -0
- package/dist/api-test/snapshot.js +150 -0
- package/dist/api-test/snapshot.js.map +1 -0
- package/dist/api-test/test-history.d.ts +48 -0
- package/dist/api-test/test-history.d.ts.map +1 -0
- package/dist/api-test/test-history.js +122 -0
- package/dist/api-test/test-history.js.map +1 -0
- package/dist/api-test/types.d.ts +174 -0
- package/dist/api-test/types.d.ts.map +1 -0
- package/dist/api-test/types.js +13 -0
- package/dist/api-test/types.js.map +1 -0
- package/dist/api-test/verify-prepare.d.ts +30 -0
- package/dist/api-test/verify-prepare.d.ts.map +1 -0
- package/dist/api-test/verify-prepare.js +150 -0
- package/dist/api-test/verify-prepare.js.map +1 -0
- package/dist/api-test/verify-write.d.ts +31 -0
- package/dist/api-test/verify-write.d.ts.map +1 -0
- package/dist/api-test/verify-write.js +159 -0
- package/dist/api-test/verify-write.js.map +1 -0
- package/dist/bin/codegraph.js +0 -100
- package/dist/bin/codegraph.js.map +1 -1
- package/dist/dbquery/dump-schema.d.ts +46 -0
- package/dist/dbquery/dump-schema.d.ts.map +1 -0
- package/dist/dbquery/dump-schema.js +379 -0
- package/dist/dbquery/dump-schema.js.map +1 -0
- package/dist/installer/targets/claude.d.ts +15 -0
- package/dist/installer/targets/claude.d.ts.map +1 -1
- package/dist/installer/targets/claude.js +53 -0
- package/dist/installer/targets/claude.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/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,667 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Related-code search — find methods / classes / files with similar names
|
|
4
|
+
* across the project. Cross-class retrieval: where `context` shows you a
|
|
5
|
+
* method's siblings in the same class, `related` shows you its peers across
|
|
6
|
+
* the whole codebase.
|
|
7
|
+
*
|
|
8
|
+
* MVP similarity metric: name-based (camelCase token overlap + Levenshtein
|
|
9
|
+
* distance + substring match). No AST signature matching, no body-shape
|
|
10
|
+
* analysis — those are explicit non-goals per clarify-requirements.
|
|
11
|
+
*
|
|
12
|
+
* Input is multi-granular:
|
|
13
|
+
* - `Class#method` — method-level: find similar methods project-wide
|
|
14
|
+
* - `Class` — class-level: find similar class/interface names
|
|
15
|
+
* - `path/to/file.java` — file-level: find files with similar basenames
|
|
16
|
+
* - bare `<name>` — fuzzy method search (like calls callers mode)
|
|
17
|
+
*
|
|
18
|
+
* Output: top-K matches, similarity-ranked. Same essential/recommended/optional
|
|
19
|
+
* tiered budget as `calls` / `impact`.
|
|
20
|
+
*
|
|
21
|
+
* Isolation: lives under `src/search/`, imports nothing from the indexed
|
|
22
|
+
* pipeline. Reuses call-sites helpers (listJavaFamilyFiles, DEFAULT_EXCLUDE_DIRS).
|
|
23
|
+
*/
|
|
24
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
27
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
28
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
29
|
+
}
|
|
30
|
+
Object.defineProperty(o, k2, desc);
|
|
31
|
+
}) : (function(o, m, k, k2) {
|
|
32
|
+
if (k2 === undefined) k2 = k;
|
|
33
|
+
o[k2] = m[k];
|
|
34
|
+
}));
|
|
35
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
36
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
37
|
+
}) : function(o, v) {
|
|
38
|
+
o["default"] = v;
|
|
39
|
+
});
|
|
40
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
41
|
+
var ownKeys = function(o) {
|
|
42
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
43
|
+
var ar = [];
|
|
44
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
45
|
+
return ar;
|
|
46
|
+
};
|
|
47
|
+
return ownKeys(o);
|
|
48
|
+
};
|
|
49
|
+
return function (mod) {
|
|
50
|
+
if (mod && mod.__esModule) return mod;
|
|
51
|
+
var result = {};
|
|
52
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
53
|
+
__setModuleDefault(result, mod);
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
})();
|
|
57
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
exports.PENALTY_GETTER_MISMATCH = exports.PENALTY_POJO_VS_DOMAIN = exports.GETTER_PREFIX = exports.POJO_RETURN_TYPES = void 0;
|
|
59
|
+
exports.searchRelated = searchRelated;
|
|
60
|
+
exports.nameSimilarity = nameSimilarity;
|
|
61
|
+
exports.camelSplit = camelSplit;
|
|
62
|
+
exports.levenshtein = levenshtein;
|
|
63
|
+
exports.semanticPenalty = semanticPenalty;
|
|
64
|
+
exports.extractMethodReturnType = extractMethodReturnType;
|
|
65
|
+
exports.deriveQueryReturnType = deriveQueryReturnType;
|
|
66
|
+
exports.applyRelatedTieredBudget = applyRelatedTieredBudget;
|
|
67
|
+
exports.formatRelatedResult = formatRelatedResult;
|
|
68
|
+
const fs_1 = require("fs");
|
|
69
|
+
const path = __importStar(require("path"));
|
|
70
|
+
const grammars_1 = require("../extraction/grammars");
|
|
71
|
+
const call_sites_1 = require("./call-sites");
|
|
72
|
+
const ast_helpers_1 = require("./ast-helpers");
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Main entry
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
async function searchRelated(projectRoot, query, options = {}) {
|
|
77
|
+
const { topK = 20, budget = 4000, languages = ['java', 'kotlin'], excludeDirs = [], minScore = 20, } = options;
|
|
78
|
+
await (0, grammars_1.loadGrammarsForLanguages)(languages);
|
|
79
|
+
const excludeSet = new Set([...call_sites_1.DEFAULT_EXCLUDE_DIRS, ...excludeDirs]);
|
|
80
|
+
// 1. Detect query kind + extract normalized name
|
|
81
|
+
const { kind, queryName } = detectQueryKind(query);
|
|
82
|
+
// 2. Walk project files
|
|
83
|
+
const allFiles = await (0, call_sites_1.listJavaFamilyFiles)(projectRoot, excludeSet);
|
|
84
|
+
// 3. Collect candidates
|
|
85
|
+
const candidates = [];
|
|
86
|
+
for (const file of allFiles) {
|
|
87
|
+
let content;
|
|
88
|
+
try {
|
|
89
|
+
content = await fs_1.promises.readFile(file, 'utf-8');
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const relPath = path.relative(projectRoot, file).replace(/\\/g, '/');
|
|
95
|
+
if (kind === 'file') {
|
|
96
|
+
// File-level: compare basenames
|
|
97
|
+
const baseName = path.basename(file).replace(/\.(java|kt|kts)$/, '');
|
|
98
|
+
const ext = path.extname(file);
|
|
99
|
+
const score = nameSimilarity(queryName, baseName);
|
|
100
|
+
if (score.score >= minScore) {
|
|
101
|
+
candidates.push({
|
|
102
|
+
kind: 'file',
|
|
103
|
+
name: baseName + ext,
|
|
104
|
+
qualifiedName: relPath,
|
|
105
|
+
filePath: relPath,
|
|
106
|
+
line: 1,
|
|
107
|
+
enclosingClass: null,
|
|
108
|
+
similarity: score.score,
|
|
109
|
+
reason: score.reason,
|
|
110
|
+
snippet: '',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
// Method / class level — parse and walk AST
|
|
116
|
+
if (!content.includes(queryName.split(/(?=[A-Z])/)[0] ?? queryName))
|
|
117
|
+
continue;
|
|
118
|
+
const language = (0, grammars_1.detectLanguage)(file, content);
|
|
119
|
+
if (!languages.includes(language))
|
|
120
|
+
continue;
|
|
121
|
+
const parser = (0, grammars_1.getParser)(language);
|
|
122
|
+
if (!parser)
|
|
123
|
+
continue;
|
|
124
|
+
const tree = parser.parse(content);
|
|
125
|
+
if (!tree)
|
|
126
|
+
continue;
|
|
127
|
+
(0, ast_helpers_1.walkTree)(tree.rootNode, (node) => {
|
|
128
|
+
if (kind === 'method') {
|
|
129
|
+
if (node.type !== 'method_declaration' &&
|
|
130
|
+
node.type !== 'constructor_declaration')
|
|
131
|
+
return;
|
|
132
|
+
const nameNode = node.childForFieldName('name');
|
|
133
|
+
if (!nameNode)
|
|
134
|
+
return;
|
|
135
|
+
const name = nameNode.text;
|
|
136
|
+
const score = nameSimilarity(queryName, name);
|
|
137
|
+
if (score.score < minScore)
|
|
138
|
+
return;
|
|
139
|
+
const enclosing = findEnclosingClassName(node);
|
|
140
|
+
const enclosingReturnType = extractMethodReturnType(node, language);
|
|
141
|
+
candidates.push({
|
|
142
|
+
kind: 'method',
|
|
143
|
+
name,
|
|
144
|
+
qualifiedName: enclosing ? `${enclosing}#${name}` : name,
|
|
145
|
+
filePath: relPath,
|
|
146
|
+
line: node.startPosition.row + 1,
|
|
147
|
+
enclosingClass: enclosing,
|
|
148
|
+
similarity: score.score,
|
|
149
|
+
reason: score.reason,
|
|
150
|
+
snippet: extractDeclarationSnippet(content, node),
|
|
151
|
+
enclosingReturnType,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
else if (kind === 'class') {
|
|
155
|
+
if (node.type !== 'class_declaration' &&
|
|
156
|
+
node.type !== 'interface_declaration' &&
|
|
157
|
+
node.type !== 'enum_declaration' &&
|
|
158
|
+
node.type !== 'record_declaration')
|
|
159
|
+
return;
|
|
160
|
+
const nameNode = node.childForFieldName('name');
|
|
161
|
+
if (!nameNode)
|
|
162
|
+
return;
|
|
163
|
+
const name = nameNode.text;
|
|
164
|
+
const score = nameSimilarity(queryName, name);
|
|
165
|
+
if (score.score < minScore)
|
|
166
|
+
return;
|
|
167
|
+
const kindMap = {
|
|
168
|
+
class_declaration: 'class',
|
|
169
|
+
interface_declaration: 'interface',
|
|
170
|
+
enum_declaration: 'enum',
|
|
171
|
+
record_declaration: 'record',
|
|
172
|
+
};
|
|
173
|
+
candidates.push({
|
|
174
|
+
kind: kindMap[node.type] ?? 'class',
|
|
175
|
+
name,
|
|
176
|
+
qualifiedName: name,
|
|
177
|
+
filePath: relPath,
|
|
178
|
+
line: node.startPosition.row + 1,
|
|
179
|
+
enclosingClass: null,
|
|
180
|
+
similarity: score.score,
|
|
181
|
+
reason: score.reason,
|
|
182
|
+
snippet: extractDeclarationSnippet(content, node),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
// 4. Apply semantic penalty (iteration-3 B-6 #6).
|
|
188
|
+
// For method queries: derive the query's likely return type from same-name
|
|
189
|
+
// candidates (most frequent), then penalize candidates whose return type
|
|
190
|
+
// or name shape indicates a different intent (e.g. a String-returning getter
|
|
191
|
+
// `getSceneNameWithCommit` shouldn't outrank a real domain-object method
|
|
192
|
+
// `sceneCommitWithXxx`).
|
|
193
|
+
if (kind === 'method') {
|
|
194
|
+
const queryMethodReturnType = deriveQueryReturnType(candidates, queryName);
|
|
195
|
+
for (const c of candidates) {
|
|
196
|
+
const baseScore = c.similarity;
|
|
197
|
+
const penalty = semanticPenalty({ name: c.name, enclosingReturnType: c.enclosingReturnType }, { name: queryName, enclosingReturnType: queryMethodReturnType });
|
|
198
|
+
if (penalty < 0) {
|
|
199
|
+
const finalScore = Math.max(0, baseScore + penalty);
|
|
200
|
+
c.similarity = finalScore;
|
|
201
|
+
const penaltyParts = [];
|
|
202
|
+
if (c.enclosingReturnType &&
|
|
203
|
+
exports.POJO_RETURN_TYPES.has(c.enclosingReturnType) &&
|
|
204
|
+
queryMethodReturnType &&
|
|
205
|
+
!exports.POJO_RETURN_TYPES.has(queryMethodReturnType)) {
|
|
206
|
+
penaltyParts.push(`-${exports.PENALTY_POJO_VS_DOMAIN} POJO return (${c.enclosingReturnType}) vs domain (${queryMethodReturnType})`);
|
|
207
|
+
}
|
|
208
|
+
if (exports.GETTER_PREFIX.test(c.name) && !exports.GETTER_PREFIX.test(queryName)) {
|
|
209
|
+
penaltyParts.push(`-${exports.PENALTY_GETTER_MISMATCH} getter prefix mismatch`);
|
|
210
|
+
}
|
|
211
|
+
c.reason = `${c.reason} · semantic penalty ${penalty} [${penaltyParts.join(', ')}] → ${finalScore}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// 5. Filter exact-self matches (the query itself shouldn't appear)
|
|
216
|
+
// If query was `Class#method`, filter out the exact `Class#method` candidate.
|
|
217
|
+
const filtered = candidates.filter((c) => {
|
|
218
|
+
if (kind === 'method' && query.includes('#')) {
|
|
219
|
+
return c.qualifiedName !== query;
|
|
220
|
+
}
|
|
221
|
+
if (kind === 'class') {
|
|
222
|
+
// Don't return the class that matches the query exactly if there's only one
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
});
|
|
227
|
+
// 6. Sort by similarity desc, then by path depth asc, then by line
|
|
228
|
+
filtered.sort((a, b) => {
|
|
229
|
+
if (a.similarity !== b.similarity)
|
|
230
|
+
return b.similarity - a.similarity;
|
|
231
|
+
const da = a.filePath.split('/').length;
|
|
232
|
+
const db = b.filePath.split('/').length;
|
|
233
|
+
if (da !== db)
|
|
234
|
+
return da - db;
|
|
235
|
+
if (a.filePath !== b.filePath)
|
|
236
|
+
return a.filePath.localeCompare(b.filePath);
|
|
237
|
+
return a.line - b.line;
|
|
238
|
+
});
|
|
239
|
+
const belowMinScore = candidates.length - filtered.length;
|
|
240
|
+
const topMatches = filtered.slice(0, topK);
|
|
241
|
+
const pastTopK = filtered.length - topMatches.length;
|
|
242
|
+
// 7. Tiered budget
|
|
243
|
+
const { tiered, truncatedByBudget } = applyRelatedTieredBudget(topMatches, budget, { forceRecommended: options.withSnippet ?? false, forceOptional: options.full ?? false });
|
|
244
|
+
return {
|
|
245
|
+
query,
|
|
246
|
+
queryKind: kind,
|
|
247
|
+
queryName,
|
|
248
|
+
totalFound: filtered.length,
|
|
249
|
+
returned: tiered.length,
|
|
250
|
+
matches: tiered,
|
|
251
|
+
omitted: {
|
|
252
|
+
belowMinScore,
|
|
253
|
+
pastTopK,
|
|
254
|
+
truncatedByBudget,
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Query kind detection
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
function detectQueryKind(query) {
|
|
262
|
+
// File path: contains `/` or `\` and ends with .java/.kt
|
|
263
|
+
if (/[/\\]/.test(query) && /\.(java|kt|kts)$/i.test(query)) {
|
|
264
|
+
return { kind: 'file', queryName: path.basename(query).replace(/\.(java|kt|kts)$/i, '') };
|
|
265
|
+
}
|
|
266
|
+
// Method: `Class#method` or `pkg.Class#method`
|
|
267
|
+
const hashIdx = query.indexOf('#');
|
|
268
|
+
if (hashIdx >= 0) {
|
|
269
|
+
return { kind: 'method', queryName: query.slice(hashIdx + 1) };
|
|
270
|
+
}
|
|
271
|
+
// File basename without path
|
|
272
|
+
if (/\.(java|kt|kts)$/i.test(query)) {
|
|
273
|
+
return { kind: 'file', queryName: query.replace(/\.(java|kt|kts)$/i, '') };
|
|
274
|
+
}
|
|
275
|
+
// Class: starts with uppercase letter, single token (no dots)
|
|
276
|
+
if (/^[A-Z][a-zA-Z0-9_]*$/.test(query)) {
|
|
277
|
+
return { kind: 'class', queryName: query };
|
|
278
|
+
}
|
|
279
|
+
// Default: method (bare name search)
|
|
280
|
+
return { kind: 'method', queryName: query };
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Score name similarity 0-100. Higher = more similar.
|
|
284
|
+
*
|
|
285
|
+
* Heuristics (first match wins):
|
|
286
|
+
* exact match (different class) → 100
|
|
287
|
+
* query is exact prefix of candidate → 85 (createOrder vs createOrderAsync)
|
|
288
|
+
* candidate is exact prefix of query → 75
|
|
289
|
+
* all query tokens appear in candidate → 75 (camelCase token overlap)
|
|
290
|
+
* some query tokens match exactly → 30 + 10 * overlap
|
|
291
|
+
* Levenshtein distance ≤ 2 → 60 - dist * 10
|
|
292
|
+
* substring match (one contains the other) → 40
|
|
293
|
+
* Levenshtein distance ≤ 5 → 25
|
|
294
|
+
* else → 0
|
|
295
|
+
*/
|
|
296
|
+
function nameSimilarity(query, candidate) {
|
|
297
|
+
if (!query || !candidate)
|
|
298
|
+
return { score: 0, reason: 'empty' };
|
|
299
|
+
const q = query.toLowerCase();
|
|
300
|
+
const c = candidate.toLowerCase();
|
|
301
|
+
// Exact match
|
|
302
|
+
if (q === c)
|
|
303
|
+
return { score: 100, reason: 'exact name match' };
|
|
304
|
+
// Prefix
|
|
305
|
+
if (c.startsWith(q)) {
|
|
306
|
+
const extra = candidate.length - query.length;
|
|
307
|
+
return { score: Math.max(70, 95 - extra * 2), reason: `candidate extends query (+${extra} chars)` };
|
|
308
|
+
}
|
|
309
|
+
if (q.startsWith(c)) {
|
|
310
|
+
const extra = query.length - candidate.length;
|
|
311
|
+
return { score: Math.max(60, 85 - extra * 2), reason: `query extends candidate (+${extra} chars)` };
|
|
312
|
+
}
|
|
313
|
+
// Token overlap (camelCase split)
|
|
314
|
+
const qTokens = camelSplit(query);
|
|
315
|
+
const cTokens = camelSplit(candidate);
|
|
316
|
+
if (qTokens.length > 0 && cTokens.length > 0) {
|
|
317
|
+
const qSet = new Set(qTokens.map((t) => t.toLowerCase()));
|
|
318
|
+
const cSet = new Set(cTokens.map((t) => t.toLowerCase()));
|
|
319
|
+
const intersection = [...qSet].filter((t) => cSet.has(t));
|
|
320
|
+
if (intersection.length === qSet.size && qSet.size > 0) {
|
|
321
|
+
// All query tokens are in candidate
|
|
322
|
+
return {
|
|
323
|
+
score: 70,
|
|
324
|
+
reason: `all ${qSet.size} query token${qSet.size === 1 ? '' : 's'} in candidate`,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (intersection.length > 0) {
|
|
328
|
+
return {
|
|
329
|
+
score: 30 + intersection.length * 10,
|
|
330
|
+
reason: `${intersection.length}/${qSet.size} query tokens match (${intersection.join(',')})`,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// Levenshtein distance
|
|
335
|
+
const dist = levenshtein(q, c);
|
|
336
|
+
if (dist <= 2 && Math.max(q.length, c.length) >= 4) {
|
|
337
|
+
return { score: 60 - dist * 10, reason: `Levenshtein distance ${dist}` };
|
|
338
|
+
}
|
|
339
|
+
// Substring
|
|
340
|
+
if (c.includes(q) || q.includes(c)) {
|
|
341
|
+
return { score: 40, reason: 'substring match' };
|
|
342
|
+
}
|
|
343
|
+
if (dist <= 5) {
|
|
344
|
+
return { score: 25, reason: `Levenshtein distance ${dist}` };
|
|
345
|
+
}
|
|
346
|
+
return { score: 0, reason: 'no similarity' };
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Split camelCase / PascalCase identifier into lowercase tokens.
|
|
350
|
+
* `createOrderAsync` → ['create', 'Order', 'Async']
|
|
351
|
+
* `OrderService` → ['Order', 'Service']
|
|
352
|
+
* `ICustomerOrderService` → ['ICustomer', 'Order', 'Service']
|
|
353
|
+
* (note: leading `I` is preserved; we don't strip interface-prefix here)
|
|
354
|
+
*/
|
|
355
|
+
function camelSplit(s) {
|
|
356
|
+
// Split at uppercase letters (camelCase boundaries), keep the uppercase letter
|
|
357
|
+
const parts = s.split(/(?=[A-Z])/).filter((p) => p.length > 0);
|
|
358
|
+
return parts;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Standard Levenshtein distance (no early termination — names are short).
|
|
362
|
+
*/
|
|
363
|
+
function levenshtein(a, b) {
|
|
364
|
+
if (a === b)
|
|
365
|
+
return 0;
|
|
366
|
+
if (a.length === 0)
|
|
367
|
+
return b.length;
|
|
368
|
+
if (b.length === 0)
|
|
369
|
+
return a.length;
|
|
370
|
+
const prev = new Array(b.length + 1);
|
|
371
|
+
const curr = new Array(b.length + 1);
|
|
372
|
+
for (let j = 0; j <= b.length; j++)
|
|
373
|
+
prev[j] = j;
|
|
374
|
+
for (let i = 1; i <= a.length; i++) {
|
|
375
|
+
curr[0] = i;
|
|
376
|
+
for (let j = 1; j <= b.length; j++) {
|
|
377
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
378
|
+
const insertCost = (curr[j - 1] ?? 0) + 1;
|
|
379
|
+
const deleteCost = (prev[j] ?? 0) + 1;
|
|
380
|
+
const substCost = (prev[j - 1] ?? 0) + cost;
|
|
381
|
+
curr[j] = Math.min(insertCost, deleteCost, substCost);
|
|
382
|
+
}
|
|
383
|
+
for (let j = 0; j <= b.length; j++)
|
|
384
|
+
prev[j] = curr[j] ?? 0;
|
|
385
|
+
}
|
|
386
|
+
return prev[b.length] ?? 0;
|
|
387
|
+
}
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Semantic penalty (iteration-3 B-6 #6)
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
//
|
|
392
|
+
// `nameSimilarity` ranks methods by name shape alone — so a getter that returns
|
|
393
|
+
// a String (`getSceneNameWithCommit`) can tie a real domain method that returns
|
|
394
|
+
// a business object (`sceneGroupCommit` → `CreateCustomerOrderRspVo`) when both
|
|
395
|
+
// share the query's tokens. `semanticPenalty` adds a non-positive adjustment on
|
|
396
|
+
// top of the base nameSimilarity score to push those shape-matched-but-semantically
|
|
397
|
+
// different candidates below real variants.
|
|
398
|
+
//
|
|
399
|
+
// Two independent rules, applied additively:
|
|
400
|
+
// 1. POJO-vs-domain mismatch: candidate returns a POJO (String/Integer/…)
|
|
401
|
+
// but the query returns a domain object → -PENALTY_POJO_VS_DOMAIN
|
|
402
|
+
// 2. Getter-prefix mismatch: candidate name starts with get/is/has but the
|
|
403
|
+
// query doesn't → -PENALTY_GETTER_MISMATCH
|
|
404
|
+
//
|
|
405
|
+
// Worst case both rules fire (-35 with default values). Final score is always
|
|
406
|
+
// clamped to >= 0 at the call site.
|
|
407
|
+
/**
|
|
408
|
+
* POJO / primitive-ish return types. A candidate whose enclosing method returns
|
|
409
|
+
* one of these is likely a simple accessor/converter, not the business method
|
|
410
|
+
* the query is after. Add more here if probe shows new patterns.
|
|
411
|
+
*/
|
|
412
|
+
exports.POJO_RETURN_TYPES = new Set([
|
|
413
|
+
'String', 'Integer', 'Long', 'Boolean', 'void', 'Float', 'Double',
|
|
414
|
+
'Object', 'Optional', 'int', 'long', 'boolean', 'float', 'double',
|
|
415
|
+
]);
|
|
416
|
+
/** Candidate name starts with `get`/`is`/`has` followed by an uppercase letter. */
|
|
417
|
+
exports.GETTER_PREFIX = /^(get|is|has)[A-Z]/;
|
|
418
|
+
/** Penalty values. Tunable range per plan: -20/-15 (initial) → -25/-20 (max). */
|
|
419
|
+
exports.PENALTY_POJO_VS_DOMAIN = -20;
|
|
420
|
+
exports.PENALTY_GETTER_MISMATCH = -15;
|
|
421
|
+
/**
|
|
422
|
+
* Returns a non-positive penalty. Higher absolute value = more penalty.
|
|
423
|
+
*
|
|
424
|
+
* Rule 1: candidate returns POJO but query returns domain object → -20
|
|
425
|
+
* Rule 2: candidate starts with get/is/has but query doesn't → -15
|
|
426
|
+
* Rules stack (worst case -35 with defaults).
|
|
427
|
+
*
|
|
428
|
+
* Pure function — no I/O, no AST. Safe to unit-test directly.
|
|
429
|
+
*/
|
|
430
|
+
function semanticPenalty(candidate, queryMethod) {
|
|
431
|
+
let penalty = 0;
|
|
432
|
+
// Rule 1: POJO vs domain mismatch.
|
|
433
|
+
if (candidate.enclosingReturnType &&
|
|
434
|
+
exports.POJO_RETURN_TYPES.has(candidate.enclosingReturnType) &&
|
|
435
|
+
queryMethod.enclosingReturnType &&
|
|
436
|
+
!exports.POJO_RETURN_TYPES.has(queryMethod.enclosingReturnType)) {
|
|
437
|
+
penalty += exports.PENALTY_POJO_VS_DOMAIN;
|
|
438
|
+
}
|
|
439
|
+
// Rule 2: getter prefix on candidate but not on query.
|
|
440
|
+
if (exports.GETTER_PREFIX.test(candidate.name) &&
|
|
441
|
+
!exports.GETTER_PREFIX.test(queryMethod.name)) {
|
|
442
|
+
penalty += exports.PENALTY_GETTER_MISMATCH;
|
|
443
|
+
}
|
|
444
|
+
return penalty;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Extract a method's declared return type from its AST node.
|
|
448
|
+
*
|
|
449
|
+
* Java `method_declaration`: read the `type` field (tree-sitter-java exposes
|
|
450
|
+
* this for explicit return types). Constructors have no return type → undefined.
|
|
451
|
+
* Kotlin `function_declaration`: scan children for the `:` token, then take the
|
|
452
|
+
* following type node's text. Omitted return type (Unit inferred) → undefined.
|
|
453
|
+
*
|
|
454
|
+
* Returns undefined for unknown node shapes (defensive — caller skips penalty).
|
|
455
|
+
*/
|
|
456
|
+
function extractMethodReturnType(node, language) {
|
|
457
|
+
if (language === 'java') {
|
|
458
|
+
if (node.type !== 'method_declaration')
|
|
459
|
+
return undefined;
|
|
460
|
+
// tree-sitter-java exposes the return type via the `type` field for explicit
|
|
461
|
+
// types. Constructors (`constructor_declaration`) have no `type` field.
|
|
462
|
+
const typeNode = node.childForFieldName('type');
|
|
463
|
+
if (!typeNode)
|
|
464
|
+
return undefined;
|
|
465
|
+
// For generic types like `List<String>`, normalize to the raw type name.
|
|
466
|
+
return normalizeReturnType(typeNode.text);
|
|
467
|
+
}
|
|
468
|
+
if (language === 'kotlin') {
|
|
469
|
+
if (node.type !== 'function_declaration')
|
|
470
|
+
return undefined;
|
|
471
|
+
// Kotlin function_declaration children: fun, simple_identifier (name),
|
|
472
|
+
// function_value_parameters, [:], [user_type], function_body.
|
|
473
|
+
// Scan for the `:` separator; the return type follows it.
|
|
474
|
+
const children = node.children;
|
|
475
|
+
for (let i = 0; i < children.length - 1; i++) {
|
|
476
|
+
const c = children[i];
|
|
477
|
+
if (c && c.type === ':' && children[i + 1]) {
|
|
478
|
+
return normalizeReturnType(children[i + 1].text);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return undefined; // No explicit return type (Unit inferred).
|
|
482
|
+
}
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Normalize a raw return-type string: strip generics (`List<String>` → `List`),
|
|
487
|
+
* strip array brackets (`String[]` → `String`), trim whitespace.
|
|
488
|
+
*/
|
|
489
|
+
function normalizeReturnType(raw) {
|
|
490
|
+
let t = raw.trim();
|
|
491
|
+
// Strip generics.
|
|
492
|
+
const genIdx = t.indexOf('<');
|
|
493
|
+
if (genIdx > 0)
|
|
494
|
+
t = t.slice(0, genIdx);
|
|
495
|
+
// Strip array brackets.
|
|
496
|
+
t = t.replace(/[\[\]]/g, '').trim();
|
|
497
|
+
return t;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Derive the query method's likely return type from collected candidates.
|
|
501
|
+
*
|
|
502
|
+
* Strategy: among same-name candidates (name === queryName), pick the most
|
|
503
|
+
* frequent non-undefined return type. This is a heuristic — we assume the
|
|
504
|
+
* query method exists in the codebase and its return type appears in the
|
|
505
|
+
* candidate set. Returns undefined if no same-name candidate has a return type.
|
|
506
|
+
*/
|
|
507
|
+
function deriveQueryReturnType(candidates, queryName) {
|
|
508
|
+
const counts = new Map();
|
|
509
|
+
for (const c of candidates) {
|
|
510
|
+
if (c.name === queryName && c.enclosingReturnType) {
|
|
511
|
+
counts.set(c.enclosingReturnType, (counts.get(c.enclosingReturnType) ?? 0) + 1);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (counts.size === 0)
|
|
515
|
+
return undefined;
|
|
516
|
+
let best;
|
|
517
|
+
let bestCount = -1;
|
|
518
|
+
for (const [rt, count] of counts) {
|
|
519
|
+
if (count > bestCount) {
|
|
520
|
+
best = rt;
|
|
521
|
+
bestCount = count;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return best;
|
|
525
|
+
}
|
|
526
|
+
// ---------------------------------------------------------------------------
|
|
527
|
+
// AST helpers
|
|
528
|
+
// ---------------------------------------------------------------------------
|
|
529
|
+
function findEnclosingClassName(node) {
|
|
530
|
+
let cur = node.parent;
|
|
531
|
+
while (cur) {
|
|
532
|
+
if (cur.type === 'class_declaration' ||
|
|
533
|
+
cur.type === 'interface_declaration' ||
|
|
534
|
+
cur.type === 'enum_declaration' ||
|
|
535
|
+
cur.type === 'record_declaration') {
|
|
536
|
+
const name = cur.childForFieldName('name')?.text;
|
|
537
|
+
if (name)
|
|
538
|
+
return name;
|
|
539
|
+
}
|
|
540
|
+
cur = cur.parent;
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
function extractDeclarationSnippet(content, node) {
|
|
545
|
+
const startRow = node.startPosition.row;
|
|
546
|
+
const lines = content.split('\n');
|
|
547
|
+
// Take the declaration start + 1-2 lines (signature usually fits in 1-3 lines)
|
|
548
|
+
const end = Math.min(startRow + 3, lines.length);
|
|
549
|
+
return lines.slice(startRow, end).join('\n').trim();
|
|
550
|
+
}
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
// Tiered budget (same shape as calls — essential / recommended / optional)
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
const TIER_ESSENTIAL_COST = 40;
|
|
555
|
+
const TIER_RECOMMENDED_ADDITIONAL = 120;
|
|
556
|
+
const TIER_OPTIONAL_ADDITIONAL = 400;
|
|
557
|
+
function applyRelatedTieredBudget(matches, budget, opts = {}) {
|
|
558
|
+
let count = 0;
|
|
559
|
+
let used = 0;
|
|
560
|
+
for (let i = 0; i < matches.length; i++) {
|
|
561
|
+
if (used + TIER_ESSENTIAL_COST > budget && count > 0)
|
|
562
|
+
break;
|
|
563
|
+
used += TIER_ESSENTIAL_COST;
|
|
564
|
+
count++;
|
|
565
|
+
}
|
|
566
|
+
const truncatedByBudget = matches.length - count;
|
|
567
|
+
const tiered = matches.slice(0, count).map((m) => ({ ...m, tier: 'essential' }));
|
|
568
|
+
if (opts.forceOptional) {
|
|
569
|
+
for (const m of tiered)
|
|
570
|
+
m.tier = 'optional';
|
|
571
|
+
return { tiered, truncatedByBudget };
|
|
572
|
+
}
|
|
573
|
+
if (opts.forceRecommended) {
|
|
574
|
+
for (const m of tiered)
|
|
575
|
+
m.tier = 'recommended';
|
|
576
|
+
return { tiered, truncatedByBudget };
|
|
577
|
+
}
|
|
578
|
+
let remaining = budget - used;
|
|
579
|
+
for (const m of tiered) {
|
|
580
|
+
if (remaining >= TIER_RECOMMENDED_ADDITIONAL) {
|
|
581
|
+
m.tier = 'recommended';
|
|
582
|
+
remaining -= TIER_RECOMMENDED_ADDITIONAL;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
for (const m of tiered) {
|
|
586
|
+
if (m.tier !== 'recommended')
|
|
587
|
+
continue;
|
|
588
|
+
if (remaining >= TIER_OPTIONAL_ADDITIONAL) {
|
|
589
|
+
m.tier = 'optional';
|
|
590
|
+
remaining -= TIER_OPTIONAL_ADDITIONAL;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return { tiered, truncatedByBudget };
|
|
594
|
+
}
|
|
595
|
+
// ---------------------------------------------------------------------------
|
|
596
|
+
// Formatter (CLI markdown)
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
function formatRelatedResult(result, options) {
|
|
599
|
+
const lines = [];
|
|
600
|
+
lines.push(`**Related to \`${result.queryName}\` (${result.queryKind})** — ${result.totalFound} match${result.totalFound === 1 ? '' : 'es'}, returning ${result.returned}`);
|
|
601
|
+
if (result.matches.length === 0) {
|
|
602
|
+
lines.push('');
|
|
603
|
+
lines.push(`_No matches above the similarity threshold. Try a broader query, or lower the threshold with \`--min-score 10\`._`);
|
|
604
|
+
return lines.join('\n');
|
|
605
|
+
}
|
|
606
|
+
// Group by similarity bucket for scanability
|
|
607
|
+
const buckets = new Map();
|
|
608
|
+
for (const m of result.matches) {
|
|
609
|
+
let bucket;
|
|
610
|
+
if (m.similarity >= 80)
|
|
611
|
+
bucket = '🟢 Strong match (≥80)';
|
|
612
|
+
else if (m.similarity >= 50)
|
|
613
|
+
bucket = '🟡 Moderate match (50-79)';
|
|
614
|
+
else if (m.similarity >= 30)
|
|
615
|
+
bucket = '⚪ Weak match (30-49)';
|
|
616
|
+
else
|
|
617
|
+
bucket = '🗄️ Fuzzy match (<30)';
|
|
618
|
+
const arr = buckets.get(bucket) ?? [];
|
|
619
|
+
arr.push(m);
|
|
620
|
+
buckets.set(bucket, arr);
|
|
621
|
+
}
|
|
622
|
+
const bucketOrder = [
|
|
623
|
+
'🟢 Strong match (≥80)',
|
|
624
|
+
'🟡 Moderate match (50-79)',
|
|
625
|
+
'⚪ Weak match (30-49)',
|
|
626
|
+
'🗄️ Fuzzy match (<30)',
|
|
627
|
+
];
|
|
628
|
+
for (const bucket of bucketOrder) {
|
|
629
|
+
const items = buckets.get(bucket);
|
|
630
|
+
if (!items || items.length === 0)
|
|
631
|
+
continue;
|
|
632
|
+
lines.push('');
|
|
633
|
+
lines.push(`### ${bucket}`);
|
|
634
|
+
for (const m of items) {
|
|
635
|
+
const kindTag = m.kind === 'method' ? '' : ` [${m.kind}]`;
|
|
636
|
+
const enclosingTag = m.enclosingClass ? ` _in ${m.enclosingClass}_` : '';
|
|
637
|
+
lines.push(`- **${m.name}**${kindTag} (score ${m.similarity}) \`${m.filePath}:${m.line}\`${enclosingTag}`);
|
|
638
|
+
lines.push(` _${m.reason}_`);
|
|
639
|
+
if (m.tier === 'recommended' || m.tier === 'optional') {
|
|
640
|
+
if (m.snippet) {
|
|
641
|
+
const snippetLines = m.snippet.split('\n').slice(0, 3);
|
|
642
|
+
for (const sl of snippetLines) {
|
|
643
|
+
lines.push(` > ${sl}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (options?.explain) {
|
|
648
|
+
lines.push(` _score: +${m.similarity} similarity (${m.reason}) = ${m.similarity}_`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// Omitted summary
|
|
653
|
+
const o = result.omitted;
|
|
654
|
+
const omittedParts = [];
|
|
655
|
+
if (o.belowMinScore > 0)
|
|
656
|
+
omittedParts.push(`${o.belowMinScore} below min-score`);
|
|
657
|
+
if (o.pastTopK > 0)
|
|
658
|
+
omittedParts.push(`${o.pastTopK} past topK`);
|
|
659
|
+
if (o.truncatedByBudget > 0)
|
|
660
|
+
omittedParts.push(`${o.truncatedByBudget} truncated by budget`);
|
|
661
|
+
if (omittedParts.length > 0) {
|
|
662
|
+
lines.push('');
|
|
663
|
+
lines.push(`_Omitted: ${omittedParts.join(' · ')}_`);
|
|
664
|
+
}
|
|
665
|
+
return lines.join('\n');
|
|
666
|
+
}
|
|
667
|
+
//# sourceMappingURL=related.js.map
|