gitnexus 1.6.2 → 1.6.3-rc.2
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/dist/_shared/index.d.ts +7 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +5 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/_shared/scope-resolution/evidence-weights.d.ts +69 -0
- package/dist/_shared/scope-resolution/evidence-weights.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/evidence-weights.js +84 -0
- package/dist/_shared/scope-resolution/evidence-weights.js.map +1 -0
- package/dist/_shared/scope-resolution/language-classification.d.ts +26 -0
- package/dist/_shared/scope-resolution/language-classification.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/language-classification.js +44 -0
- package/dist/_shared/scope-resolution/language-classification.js.map +1 -0
- package/dist/_shared/scope-resolution/origin-priority.d.ts +14 -0
- package/dist/_shared/scope-resolution/origin-priority.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/origin-priority.js +21 -0
- package/dist/_shared/scope-resolution/origin-priority.js.map +1 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts +34 -0
- package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/symbol-definition.js +12 -0
- package/dist/_shared/scope-resolution/symbol-definition.js.map +1 -0
- package/dist/_shared/scope-resolution/types.d.ts +200 -0
- package/dist/_shared/scope-resolution/types.d.ts.map +1 -0
- package/dist/_shared/scope-resolution/types.js +17 -0
- package/dist/_shared/scope-resolution/types.js.map +1 -0
- package/dist/core/ingestion/call-processor.d.ts +2 -1
- package/dist/core/ingestion/model/field-registry.d.ts +1 -1
- package/dist/core/ingestion/model/index.d.ts +1 -1
- package/dist/core/ingestion/model/index.js +2 -0
- package/dist/core/ingestion/model/method-registry.d.ts +1 -1
- package/dist/core/ingestion/model/registration-table.d.ts +1 -2
- package/dist/core/ingestion/model/resolution-context.d.ts +1 -1
- package/dist/core/ingestion/model/resolve.d.ts +1 -1
- package/dist/core/ingestion/model/symbol-table.d.ts +1 -23
- package/dist/core/ingestion/model/type-registry.d.ts +1 -1
- package/dist/mcp/local/local-backend.d.ts +48 -1
- package/dist/mcp/local/local-backend.js +287 -131
- package/dist/mcp/tools.js +19 -1
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* `SymbolTable` by design.
|
|
11
11
|
*/
|
|
12
12
|
export { type SemanticModel, type MutableSemanticModel, createSemanticModel, } from './semantic-model.js';
|
|
13
|
-
export { type SymbolTableReader, type SymbolTableWriter, createSymbolTable, type
|
|
13
|
+
export { type SymbolTableReader, type SymbolTableWriter, createSymbolTable, type AddMetadata, CLASS_TYPES, CLASS_TYPES_TUPLE, type ClassLikeLabel, FREE_CALLABLE_TYPES, FREE_CALLABLE_TUPLE, type FreeCallableLabel, CALL_TARGET_TYPES, } from './symbol-table.js';
|
|
14
14
|
export { type TypeRegistry, type MutableTypeRegistry, createTypeRegistry, } from './type-registry.js';
|
|
15
15
|
export { type MethodRegistry, type MutableMethodRegistry, createMethodRegistry, } from './method-registry.js';
|
|
16
16
|
export { type FieldRegistry, type MutableFieldRegistry, createFieldRegistry, } from './field-registry.js';
|
|
@@ -17,6 +17,8 @@ export { createSemanticModel, } from './semantic-model.js';
|
|
|
17
17
|
// for the rare caller that needs the file/callable interface in
|
|
18
18
|
// isolation (e.g. tests).
|
|
19
19
|
export { createSymbolTable, CLASS_TYPES, CLASS_TYPES_TUPLE, FREE_CALLABLE_TYPES, FREE_CALLABLE_TUPLE, CALL_TARGET_TYPES, } from './symbol-table.js';
|
|
20
|
+
// `SymbolDefinition` moved to `gitnexus-shared` (RFC #909 Ring 1 #910).
|
|
21
|
+
// Consumers should import it directly from `gitnexus-shared`, not via this barrel.
|
|
20
22
|
// Type registry (classes, structs, interfaces, enums, records, impls)
|
|
21
23
|
export { createTypeRegistry, } from './type-registry.js';
|
|
22
24
|
// Method registry (owner-scoped methods with arity-aware overload lookup)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `ownerNodeId\0methodName` for O(1) lookup. Supports overloads
|
|
7
7
|
* (array values) and arity-based filtering.
|
|
8
8
|
*/
|
|
9
|
-
import type { SymbolDefinition } from '
|
|
9
|
+
import type { SymbolDefinition } from '../../../_shared/index.js';
|
|
10
10
|
export interface MethodRegistry {
|
|
11
11
|
/**
|
|
12
12
|
* Look up a method by owner class + name, optionally filtered by arity.
|
|
@@ -48,8 +48,7 @@
|
|
|
48
48
|
* The runtime exhaustiveness guard in `symbol-table.ts` will warn if a
|
|
49
49
|
* `NodeLabel` is missing from all three sets.
|
|
50
50
|
*/
|
|
51
|
-
import type { NodeLabel } from '../../../_shared/index.js';
|
|
52
|
-
import type { SymbolDefinition } from './symbol-table.js';
|
|
51
|
+
import type { NodeLabel, SymbolDefinition } from '../../../_shared/index.js';
|
|
53
52
|
import type { MutableTypeRegistry } from './type-registry.js';
|
|
54
53
|
import type { MutableMethodRegistry } from './method-registry.js';
|
|
55
54
|
import type { MutableFieldRegistry } from './field-registry.js';
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - Tier 3 combines lookupClassByName + lookupImplByName + lookupCallableByName
|
|
18
18
|
* (three O(1) index lookups with a narrow, type-specific result set).
|
|
19
19
|
*/
|
|
20
|
-
import type { SymbolDefinition } from '
|
|
20
|
+
import type { SymbolDefinition } from '../../../_shared/index.js';
|
|
21
21
|
import type { MutableSemanticModel } from './semantic-model.js';
|
|
22
22
|
/**
|
|
23
23
|
* A single named binding in a source file (e.g. `import { User as U }`).
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* using only the SemanticModel registries and HeritageMap — NO dependency
|
|
6
6
|
* on resolution-context.ts (circular dependency risk).
|
|
7
7
|
*/
|
|
8
|
-
import type { SymbolDefinition } from '
|
|
8
|
+
import type { SymbolDefinition } from '../../../_shared/index.js';
|
|
9
9
|
import type { SemanticModel } from './semantic-model.js';
|
|
10
10
|
import type { HeritageMap } from './heritage-map.js';
|
|
11
11
|
import type { MroStrategy } from '../../../_shared/index.js';
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* import from `./model/` here, you are going the wrong way — move the
|
|
34
34
|
* logic up the dependency chain instead.
|
|
35
35
|
*/
|
|
36
|
-
import type { NodeLabel } from '../../../_shared/index.js';
|
|
36
|
+
import type { NodeLabel, SymbolDefinition } from '../../../_shared/index.js';
|
|
37
37
|
/**
|
|
38
38
|
* Class-like NodeLabels — used for qualifiedName fallback inside
|
|
39
39
|
* `SymbolTable.add()` and (via import into `model/registration-table.ts`)
|
|
@@ -89,28 +89,6 @@ export declare const FREE_CALLABLE_TYPES: ReadonlySet<NodeLabel>;
|
|
|
89
89
|
* `filterCallableCandidates` / `countCallableCandidates`.
|
|
90
90
|
*/
|
|
91
91
|
export declare const CALL_TARGET_TYPES: ReadonlySet<NodeLabel>;
|
|
92
|
-
export interface SymbolDefinition {
|
|
93
|
-
nodeId: string;
|
|
94
|
-
filePath: string;
|
|
95
|
-
type: NodeLabel;
|
|
96
|
-
/** Canonical dot-separated qualified type name for class-like symbols
|
|
97
|
-
* (e.g. `App.Models.User`). Falls back to the simple symbol name when no
|
|
98
|
-
* package/namespace/module scope exists or no explicit qualified metadata is provided. */
|
|
99
|
-
qualifiedName?: string;
|
|
100
|
-
parameterCount?: number;
|
|
101
|
-
/** Number of required (non-optional, non-default) parameters.
|
|
102
|
-
* Enables range-based arity filtering: argCount >= requiredParameterCount && argCount <= parameterCount. */
|
|
103
|
-
requiredParameterCount?: number;
|
|
104
|
-
/** Per-parameter type names for overload disambiguation (e.g. ['int', 'String']).
|
|
105
|
-
* Populated when parameter types are resolvable from AST (any typed language). */
|
|
106
|
-
parameterTypes?: string[];
|
|
107
|
-
/** Raw return type text extracted from AST (e.g. 'User', 'Promise<User>') */
|
|
108
|
-
returnType?: string;
|
|
109
|
-
/** Declared type for non-callable symbols — fields/properties (e.g. 'Address', 'List<User>') */
|
|
110
|
-
declaredType?: string;
|
|
111
|
-
/** Links Method/Constructor/Property to owning Class/Struct/Trait nodeId */
|
|
112
|
-
ownerId?: string;
|
|
113
|
-
}
|
|
114
92
|
/**
|
|
115
93
|
* Optional metadata accepted by {@link SymbolTable.add}. Kept as a separate
|
|
116
94
|
* type alias so callers and wrappers can share the same shape.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Eagerly-populated indexes keyed by symbol name and qualified name.
|
|
6
6
|
* Also includes a separate index for Rust Impl blocks.
|
|
7
7
|
*/
|
|
8
|
-
import type { SymbolDefinition } from '
|
|
8
|
+
import type { SymbolDefinition } from '../../../_shared/index.js';
|
|
9
9
|
export interface TypeRegistry {
|
|
10
10
|
/**
|
|
11
11
|
* Look up class-like definitions (Class, Struct, Interface, Enum, Record, Trait)
|
|
@@ -150,9 +150,56 @@ export declare class LocalBackend {
|
|
|
150
150
|
*/
|
|
151
151
|
private aggregateClusters;
|
|
152
152
|
private overview;
|
|
153
|
+
/**
|
|
154
|
+
* Patch the `type` field on candidates whose `labels(n)[0]` projection
|
|
155
|
+
* came back empty — a known LadybugDB behaviour for several node types.
|
|
156
|
+
*
|
|
157
|
+
* Uses one scoped UNION query across the five priority labels rather
|
|
158
|
+
* than per-candidate round-trips, so cost is a single DB call regardless
|
|
159
|
+
* of how many candidates need enrichment. No-op when every candidate
|
|
160
|
+
* already has a non-empty type.
|
|
161
|
+
*
|
|
162
|
+
* Failures are swallowed: label enrichment is an optimisation for
|
|
163
|
+
* downstream scoring and #480 Class/Interface BFS seeding; if it fails
|
|
164
|
+
* the symbol still resolves, just without the kind-priority bonus.
|
|
165
|
+
*/
|
|
166
|
+
private enrichCandidateLabels;
|
|
167
|
+
/**
|
|
168
|
+
* Score a symbol candidate for disambiguation ranking.
|
|
169
|
+
*
|
|
170
|
+
* Deterministic, no DB round-trip:
|
|
171
|
+
* - base 0.50
|
|
172
|
+
* - +0.40 when file_path hint matches (substring, case-insensitive)
|
|
173
|
+
* - +0.20 when kind hint exactly matches the candidate's kind
|
|
174
|
+
* - when no kind hint, a small priority bonus (Class > Interface >
|
|
175
|
+
* Function > Method > Constructor) to preserve the intuition that
|
|
176
|
+
* class-level names are usually what the user wanted.
|
|
177
|
+
*
|
|
178
|
+
* Capped at 1.0. Intentionally simple and inspectable — a future v2 can
|
|
179
|
+
* plug in BM25/embedding signals here without changing the surrounding
|
|
180
|
+
* resolver shape.
|
|
181
|
+
*/
|
|
182
|
+
private scoreCandidate;
|
|
183
|
+
/**
|
|
184
|
+
* Shared symbol resolver used by `context` and `impact`.
|
|
185
|
+
*
|
|
186
|
+
* Returns one of:
|
|
187
|
+
* - `{ kind: 'ok', symbol, resolvedLabel }` — single confident match
|
|
188
|
+
* (either direct UID, only one candidate after filtering, Class/
|
|
189
|
+
* Constructor collapse, or a top-scoring candidate with a clear gap
|
|
190
|
+
* to the runner-up).
|
|
191
|
+
* - `{ kind: 'ambiguous', candidates }` — multiple viable matches,
|
|
192
|
+
* sorted by score desc. Each candidate carries a relevance score.
|
|
193
|
+
* - `{ kind: 'not_found' }` — no matches at all.
|
|
194
|
+
*
|
|
195
|
+
* Preserves the #480 Class/Constructor preference: when the only
|
|
196
|
+
* ambiguity is between a Class and its own Constructor (same name,
|
|
197
|
+
* same filePath), the Class wins silently.
|
|
198
|
+
*/
|
|
199
|
+
private resolveSymbolCandidates;
|
|
153
200
|
/**
|
|
154
201
|
* Context tool — 360-degree symbol view with categorized refs.
|
|
155
|
-
* Disambiguation when multiple symbols share a name.
|
|
202
|
+
* Disambiguation (ranked) when multiple symbols share a name.
|
|
156
203
|
* UID-based direct lookup. No cluster in output.
|
|
157
204
|
*/
|
|
158
205
|
private context;
|
|
@@ -908,108 +908,268 @@ export class LocalBackend {
|
|
|
908
908
|
return result;
|
|
909
909
|
}
|
|
910
910
|
/**
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
911
|
+
* Patch the `type` field on candidates whose `labels(n)[0]` projection
|
|
912
|
+
* came back empty — a known LadybugDB behaviour for several node types.
|
|
913
|
+
*
|
|
914
|
+
* Uses one scoped UNION query across the five priority labels rather
|
|
915
|
+
* than per-candidate round-trips, so cost is a single DB call regardless
|
|
916
|
+
* of how many candidates need enrichment. No-op when every candidate
|
|
917
|
+
* already has a non-empty type.
|
|
918
|
+
*
|
|
919
|
+
* Failures are swallowed: label enrichment is an optimisation for
|
|
920
|
+
* downstream scoring and #480 Class/Interface BFS seeding; if it fails
|
|
921
|
+
* the symbol still resolves, just without the kind-priority bonus.
|
|
914
922
|
*/
|
|
915
|
-
async
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
MATCH (n
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
const
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
}
|
|
938
|
-
else if (isQualified) {
|
|
939
|
-
whereClause = `WHERE n.id = $symName OR n.name = $symName`;
|
|
940
|
-
queryParams = { symName: name };
|
|
923
|
+
async enrichCandidateLabels(repo, candidates) {
|
|
924
|
+
const ids = candidates.filter((c) => c.type === '' && c.id).map((c) => c.id);
|
|
925
|
+
if (ids.length === 0)
|
|
926
|
+
return;
|
|
927
|
+
try {
|
|
928
|
+
const rows = await executeParameterized(repo.id, `
|
|
929
|
+
MATCH (n:\`Class\`) WHERE n.id IN $ids RETURN n.id AS id, 'Class' AS label
|
|
930
|
+
UNION ALL
|
|
931
|
+
MATCH (n:\`Interface\`) WHERE n.id IN $ids RETURN n.id AS id, 'Interface' AS label
|
|
932
|
+
UNION ALL
|
|
933
|
+
MATCH (n:\`Function\`) WHERE n.id IN $ids RETURN n.id AS id, 'Function' AS label
|
|
934
|
+
UNION ALL
|
|
935
|
+
MATCH (n:\`Method\`) WHERE n.id IN $ids RETURN n.id AS id, 'Method' AS label
|
|
936
|
+
UNION ALL
|
|
937
|
+
MATCH (n:\`Constructor\`) WHERE n.id IN $ids RETURN n.id AS id, 'Constructor' AS label
|
|
938
|
+
`, { ids });
|
|
939
|
+
const labelById = new Map();
|
|
940
|
+
for (const r of rows) {
|
|
941
|
+
const id = (r.id ?? r[0]);
|
|
942
|
+
const label = (r.label ?? r[1]);
|
|
943
|
+
if (id && label && !labelById.has(id))
|
|
944
|
+
labelById.set(id, label);
|
|
941
945
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
946
|
+
for (const c of candidates) {
|
|
947
|
+
if (c.type === '' && labelById.has(c.id))
|
|
948
|
+
c.type = labelById.get(c.id);
|
|
945
949
|
}
|
|
946
|
-
symbols = await executeParameterized(repo.id, `
|
|
947
|
-
MATCH (n) ${whereClause}
|
|
948
|
-
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
|
|
949
|
-
LIMIT 10
|
|
950
|
-
`, queryParams);
|
|
951
950
|
}
|
|
952
|
-
|
|
953
|
-
|
|
951
|
+
catch {
|
|
952
|
+
/* best-effort — downstream resolvers still work without the label */
|
|
954
953
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Score a symbol candidate for disambiguation ranking.
|
|
957
|
+
*
|
|
958
|
+
* Deterministic, no DB round-trip:
|
|
959
|
+
* - base 0.50
|
|
960
|
+
* - +0.40 when file_path hint matches (substring, case-insensitive)
|
|
961
|
+
* - +0.20 when kind hint exactly matches the candidate's kind
|
|
962
|
+
* - when no kind hint, a small priority bonus (Class > Interface >
|
|
963
|
+
* Function > Method > Constructor) to preserve the intuition that
|
|
964
|
+
* class-level names are usually what the user wanted.
|
|
965
|
+
*
|
|
966
|
+
* Capped at 1.0. Intentionally simple and inspectable — a future v2 can
|
|
967
|
+
* plug in BM25/embedding signals here without changing the surrounding
|
|
968
|
+
* resolver shape.
|
|
969
|
+
*/
|
|
970
|
+
scoreCandidate(c, hints) {
|
|
971
|
+
let s = 0.5;
|
|
972
|
+
if (hints.file_path && c.filePath && typeof c.filePath === 'string') {
|
|
973
|
+
if (c.filePath.toLowerCase().includes(hints.file_path.toLowerCase())) {
|
|
974
|
+
s += 0.4;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (hints.kind && c.kind === hints.kind) {
|
|
978
|
+
s += 0.2;
|
|
979
|
+
}
|
|
980
|
+
if (!hints.kind) {
|
|
981
|
+
const priority = {
|
|
982
|
+
Class: 5,
|
|
983
|
+
Interface: 4,
|
|
984
|
+
Function: 3,
|
|
985
|
+
Method: 2,
|
|
986
|
+
Constructor: 1,
|
|
987
|
+
};
|
|
988
|
+
s += (priority[c.kind] ?? 0) * 0.02;
|
|
989
|
+
}
|
|
990
|
+
return Math.min(1.0, s);
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Shared symbol resolver used by `context` and `impact`.
|
|
994
|
+
*
|
|
995
|
+
* Returns one of:
|
|
996
|
+
* - `{ kind: 'ok', symbol, resolvedLabel }` — single confident match
|
|
997
|
+
* (either direct UID, only one candidate after filtering, Class/
|
|
998
|
+
* Constructor collapse, or a top-scoring candidate with a clear gap
|
|
999
|
+
* to the runner-up).
|
|
1000
|
+
* - `{ kind: 'ambiguous', candidates }` — multiple viable matches,
|
|
1001
|
+
* sorted by score desc. Each candidate carries a relevance score.
|
|
1002
|
+
* - `{ kind: 'not_found' }` — no matches at all.
|
|
1003
|
+
*
|
|
1004
|
+
* Preserves the #480 Class/Constructor preference: when the only
|
|
1005
|
+
* ambiguity is between a Class and its own Constructor (same name,
|
|
1006
|
+
* same filePath), the Class wins silently.
|
|
1007
|
+
*/
|
|
1008
|
+
async resolveSymbolCandidates(repo, query, hints) {
|
|
1009
|
+
const { uid, name, include_content } = query;
|
|
1010
|
+
const selectClause = `n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}`;
|
|
1011
|
+
// Direct UID — zero-ambiguity path.
|
|
1012
|
+
if (uid) {
|
|
1013
|
+
const rows = await executeParameterized(repo.id, `MATCH (n {id: $uid}) RETURN ${selectClause} LIMIT 1`, { uid });
|
|
1014
|
+
if (rows.length === 0)
|
|
1015
|
+
return { kind: 'not_found' };
|
|
1016
|
+
const r = rows[0];
|
|
1017
|
+
const symbol = {
|
|
1018
|
+
id: (r.id ?? r[0]),
|
|
1019
|
+
name: (r.name ?? r[1]),
|
|
1020
|
+
type: (r.type ?? r[2] ?? ''),
|
|
1021
|
+
filePath: (r.filePath ?? r[3]),
|
|
1022
|
+
startLine: (r.startLine ?? r[4]),
|
|
1023
|
+
endLine: (r.endLine ?? r[5]),
|
|
1024
|
+
...(include_content ? { content: (r.content ?? r[6]) } : {}),
|
|
1025
|
+
};
|
|
1026
|
+
// Same LadybugDB label-enrichment as the name-based path: a UID
|
|
1027
|
+
// pointing at a Class must still surface `type: 'Class'` so impact's
|
|
1028
|
+
// Class/Interface BFS seed fires. No-op when type is already set.
|
|
1029
|
+
await this.enrichCandidateLabels(repo, [symbol]);
|
|
1030
|
+
return { kind: 'ok', symbol, resolvedLabel: symbol.type };
|
|
1031
|
+
}
|
|
1032
|
+
if (!name)
|
|
1033
|
+
return { kind: 'not_found' };
|
|
1034
|
+
const isQualified = name.includes('/') || name.includes(':');
|
|
1035
|
+
let whereClause;
|
|
1036
|
+
const queryParams = { symName: name };
|
|
1037
|
+
if (hints.file_path) {
|
|
1038
|
+
whereClause = `WHERE n.name = $symName AND n.filePath CONTAINS $filePath`;
|
|
1039
|
+
queryParams.filePath = hints.file_path;
|
|
1040
|
+
}
|
|
1041
|
+
else if (isQualified) {
|
|
1042
|
+
whereClause = `WHERE n.id = $symName OR n.name = $symName`;
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
whereClause = `WHERE n.name = $symName`;
|
|
1046
|
+
}
|
|
1047
|
+
// LIMIT 20 (was 10) — scoring is the point now, so give the ranker
|
|
1048
|
+
// headroom instead of arbitrary truncation.
|
|
1049
|
+
const rows = await executeParameterized(repo.id, `MATCH (n) ${whereClause} RETURN ${selectClause} LIMIT 20`, queryParams);
|
|
1050
|
+
if (rows.length === 0)
|
|
1051
|
+
return { kind: 'not_found' };
|
|
1052
|
+
// Normalise row shape across object / tuple returns from LadybugDB.
|
|
1053
|
+
const normalized = rows.map((r) => ({
|
|
1054
|
+
id: (r.id ?? r[0]),
|
|
1055
|
+
name: (r.name ?? r[1]),
|
|
1056
|
+
type: (r.type ?? r[2] ?? ''),
|
|
1057
|
+
filePath: (r.filePath ?? r[3]),
|
|
1058
|
+
startLine: (r.startLine ?? r[4]),
|
|
1059
|
+
endLine: (r.endLine ?? r[5]),
|
|
1060
|
+
...(include_content ? { content: (r.content ?? r[6]) } : {}),
|
|
1061
|
+
}));
|
|
1062
|
+
// Enrich labels for any candidates where `labels(n)[0]` came back empty.
|
|
1063
|
+
// LadybugDB returns an empty string for that projection on certain node
|
|
1064
|
+
// types (notably Class), which left downstream consumers (impact's
|
|
1065
|
+
// Class/Interface BFS seed, the kind-priority scoring bonus) unable to
|
|
1066
|
+
// distinguish a Class target from "unknown kind". One scoped UNION
|
|
1067
|
+
// across the five priority labels patches the type in-place without
|
|
1068
|
+
// per-candidate round-trips.
|
|
1069
|
+
await this.enrichCandidateLabels(repo, normalized);
|
|
1070
|
+
// Preserve #480 Class/Constructor collapse: if we have exactly one
|
|
1071
|
+
// Class (or Interface) candidate and one Constructor sharing name +
|
|
1072
|
+
// filePath, fold into the Class. This used to require a follow-up
|
|
1073
|
+
// label query because LadybugDB sometimes returns an empty labels()[0]
|
|
1074
|
+
// for Class nodes — enrichment above handles the empty-type case, but
|
|
1075
|
+
// the `type === 'Constructor'` gate still correctly triggers when a
|
|
1076
|
+
// Class and its Constructor share the name.
|
|
1077
|
+
if (!hints.kind && normalized.length > 1) {
|
|
1078
|
+
const ambiguousType = normalized.some((s) => s.type === '' || s.type === 'Constructor');
|
|
1079
|
+
if (ambiguousType) {
|
|
1080
|
+
const candidateIds = normalized.map((s) => s.id).filter(Boolean);
|
|
1081
|
+
for (const label of ['Class', 'Interface']) {
|
|
1082
|
+
const labelRows = await executeParameterized(repo.id, `MATCH (n:\`${label}\`) WHERE n.id IN $candidateIds RETURN n.id AS id LIMIT 1`, { candidateIds }).catch(() => []);
|
|
1083
|
+
if (labelRows.length > 0) {
|
|
1084
|
+
const preferredId = labelRows[0].id ?? labelRows[0][0];
|
|
1085
|
+
const preferred = normalized.find((s) => s.id === preferredId);
|
|
987
1086
|
if (preferred) {
|
|
988
|
-
|
|
989
|
-
|
|
1087
|
+
return {
|
|
1088
|
+
kind: 'ok',
|
|
1089
|
+
symbol: preferred,
|
|
1090
|
+
resolvedLabel: label,
|
|
1091
|
+
};
|
|
990
1092
|
}
|
|
991
1093
|
}
|
|
992
1094
|
}
|
|
993
|
-
if (preferred)
|
|
994
|
-
symbols = [preferred];
|
|
995
1095
|
}
|
|
996
1096
|
}
|
|
997
|
-
if (
|
|
1097
|
+
if (normalized.length === 1) {
|
|
1098
|
+
return {
|
|
1099
|
+
kind: 'ok',
|
|
1100
|
+
symbol: normalized[0],
|
|
1101
|
+
resolvedLabel: '',
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
// Score, sort desc, stable tiebreak on shorter filePath then lex uid.
|
|
1105
|
+
const scored = normalized.map((s) => ({
|
|
1106
|
+
...s,
|
|
1107
|
+
score: this.scoreCandidate({ kind: s.type, filePath: s.filePath || '' }, hints),
|
|
1108
|
+
}));
|
|
1109
|
+
scored.sort((a, b) => {
|
|
1110
|
+
if (b.score !== a.score)
|
|
1111
|
+
return b.score - a.score;
|
|
1112
|
+
const fpA = (a.filePath || '').length;
|
|
1113
|
+
const fpB = (b.filePath || '').length;
|
|
1114
|
+
if (fpA !== fpB)
|
|
1115
|
+
return fpA - fpB;
|
|
1116
|
+
return String(a.id).localeCompare(String(b.id));
|
|
1117
|
+
});
|
|
1118
|
+
// Confident single-result: top score ≥ 0.95 AND beats runner-up by a
|
|
1119
|
+
// clear margin. This lets a very strong file_path/kind hint resolve
|
|
1120
|
+
// cleanly instead of forcing the caller through a disambiguation
|
|
1121
|
+
// round-trip.
|
|
1122
|
+
//
|
|
1123
|
+
// The gap threshold uses `> 0.09` rather than `>= 0.10` on purpose:
|
|
1124
|
+
// IEEE754 addition of the scoring terms (0.50 + 0.40 + 0.20 - 0.90
|
|
1125
|
+
// yields 0.09999999999999998, not exactly 0.10) would otherwise break
|
|
1126
|
+
// the comparison for legitimate "top is 1.00, runner is 0.90" cases.
|
|
1127
|
+
// The intent is a clearly-dominant winner; 0.09 is a large enough
|
|
1128
|
+
// margin to mean that unambiguously.
|
|
1129
|
+
//
|
|
1130
|
+
// The `scored.length >= 2` guard is defensive. The `normalized.length === 1`
|
|
1131
|
+
// early return above already handles the single-candidate path, so in
|
|
1132
|
+
// practice `scored` always has at least two elements by the time we get
|
|
1133
|
+
// here — keeping the guard means changes to the upstream early-return
|
|
1134
|
+
// logic cannot accidentally index out of bounds at `scored[1]`.
|
|
1135
|
+
if (scored.length >= 2 && scored[0].score >= 0.95 && scored[0].score - scored[1].score > 0.09) {
|
|
1136
|
+
return { kind: 'ok', symbol: scored[0], resolvedLabel: scored[0].type };
|
|
1137
|
+
}
|
|
1138
|
+
return { kind: 'ambiguous', candidates: scored };
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Context tool — 360-degree symbol view with categorized refs.
|
|
1142
|
+
* Disambiguation (ranked) when multiple symbols share a name.
|
|
1143
|
+
* UID-based direct lookup. No cluster in output.
|
|
1144
|
+
*/
|
|
1145
|
+
async context(repo, params) {
|
|
1146
|
+
await this.ensureInitialized(repo.id);
|
|
1147
|
+
const { name, uid, file_path, kind, include_content } = params;
|
|
1148
|
+
if (!name && !uid) {
|
|
1149
|
+
return { error: 'Either "name" or "uid" parameter is required.' };
|
|
1150
|
+
}
|
|
1151
|
+
const outcome = await this.resolveSymbolCandidates(repo, { uid, name, include_content }, { file_path, kind });
|
|
1152
|
+
if (outcome.kind === 'not_found') {
|
|
1153
|
+
return { error: `Symbol '${name || uid}' not found` };
|
|
1154
|
+
}
|
|
1155
|
+
if (outcome.kind === 'ambiguous') {
|
|
998
1156
|
return {
|
|
999
1157
|
status: 'ambiguous',
|
|
1000
|
-
message: `Found ${
|
|
1001
|
-
candidates:
|
|
1002
|
-
uid:
|
|
1003
|
-
name:
|
|
1004
|
-
kind:
|
|
1005
|
-
filePath:
|
|
1006
|
-
line:
|
|
1158
|
+
message: `Found ${outcome.candidates.length} symbols matching '${name}'. Use uid, file_path, or kind to disambiguate.`,
|
|
1159
|
+
candidates: outcome.candidates.map((c) => ({
|
|
1160
|
+
uid: c.id,
|
|
1161
|
+
name: c.name,
|
|
1162
|
+
kind: c.type,
|
|
1163
|
+
filePath: c.filePath,
|
|
1164
|
+
line: c.startLine,
|
|
1165
|
+
score: Number(c.score.toFixed(2)),
|
|
1007
1166
|
})),
|
|
1008
1167
|
};
|
|
1009
1168
|
}
|
|
1010
1169
|
// Step 3: Build full context
|
|
1011
|
-
const sym =
|
|
1012
|
-
const
|
|
1170
|
+
const sym = outcome.symbol;
|
|
1171
|
+
const resolvedLabel = outcome.resolvedLabel;
|
|
1172
|
+
const symId = sym.id;
|
|
1013
1173
|
// Categorized incoming refs
|
|
1014
1174
|
const incomingRows = await executeParameterized(repo.id, `
|
|
1015
1175
|
MATCH (caller)-[r:CodeRelation]->(n {id: $symId})
|
|
@@ -1608,54 +1768,50 @@ export class LocalBackend {
|
|
|
1608
1768
|
];
|
|
1609
1769
|
const includeTests = params.includeTests ?? false;
|
|
1610
1770
|
const minConfidence = params.minConfidence ?? 0;
|
|
1611
|
-
// Resolve target
|
|
1612
|
-
//
|
|
1613
|
-
//
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
RETURN n.id AS id, n.name AS name, n.filePath AS filePath, 3 AS priority LIMIT 1
|
|
1631
|
-
UNION ALL
|
|
1632
|
-
MATCH (n:\`Constructor\`) WHERE n.name = $targetName
|
|
1633
|
-
RETURN n.id AS id, n.name AS name, n.filePath AS filePath, 4 AS priority LIMIT 1
|
|
1634
|
-
`, { targetName: target }).catch(() => []);
|
|
1635
|
-
if (rows.length > 0) {
|
|
1636
|
-
// Pick the row with the lowest priority value (Class wins over Constructor)
|
|
1637
|
-
const best = rows.reduce((a, b) => (a.priority ?? a[3] ?? 99) <= (b.priority ?? b[3] ?? 99) ? a : b);
|
|
1638
|
-
sym = best;
|
|
1639
|
-
const priorityToLabel = ['Class', 'Interface', 'Function', 'Method', 'Constructor'];
|
|
1640
|
-
symType = priorityToLabel[best.priority ?? best[3]] ?? '';
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
catch {
|
|
1644
|
-
/* fall through to unlabeled match */
|
|
1771
|
+
// Resolve target via the shared symbol resolver. When the caller passes
|
|
1772
|
+
// target_uid we skip the name lookup entirely (zero-ambiguity). Otherwise
|
|
1773
|
+
// we rank candidates (#470) and either proceed with a confident single
|
|
1774
|
+
// match, or return a structured ambiguous response instead of silently
|
|
1775
|
+
// picking the wrong symbol.
|
|
1776
|
+
//
|
|
1777
|
+
// The resolver preserves the #480 Class/Constructor preference heuristic:
|
|
1778
|
+
// when a Class and its Constructor share name + filePath, the Class is
|
|
1779
|
+
// selected silently.
|
|
1780
|
+
const outcome = await this.resolveSymbolCandidates(repo, { uid: params.target_uid, name: target }, { file_path: params.file_path, kind: params.kind });
|
|
1781
|
+
if (outcome.kind === 'not_found') {
|
|
1782
|
+
const missing = params.target_uid ?? target;
|
|
1783
|
+
return {
|
|
1784
|
+
error: `Target '${missing}' not found`,
|
|
1785
|
+
target: { name: target },
|
|
1786
|
+
direction,
|
|
1787
|
+
impactedCount: 0,
|
|
1788
|
+
risk: 'UNKNOWN',
|
|
1789
|
+
};
|
|
1645
1790
|
}
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1791
|
+
if (outcome.kind === 'ambiguous') {
|
|
1792
|
+
return {
|
|
1793
|
+
status: 'ambiguous',
|
|
1794
|
+
message: `Found ${outcome.candidates.length} symbols matching '${target}'. Use target_uid, file_path, or kind to disambiguate.`,
|
|
1795
|
+
target: { name: target },
|
|
1796
|
+
direction,
|
|
1797
|
+
impactedCount: 0,
|
|
1798
|
+
risk: 'UNKNOWN',
|
|
1799
|
+
candidates: outcome.candidates.map((c) => ({
|
|
1800
|
+
uid: c.id,
|
|
1801
|
+
name: c.name,
|
|
1802
|
+
kind: c.type,
|
|
1803
|
+
filePath: c.filePath,
|
|
1804
|
+
line: c.startLine,
|
|
1805
|
+
score: Number(c.score.toFixed(2)),
|
|
1806
|
+
})),
|
|
1807
|
+
};
|
|
1656
1808
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1809
|
+
const sym = {
|
|
1810
|
+
id: outcome.symbol.id,
|
|
1811
|
+
name: outcome.symbol.name,
|
|
1812
|
+
filePath: outcome.symbol.filePath,
|
|
1813
|
+
};
|
|
1814
|
+
const symType = outcome.resolvedLabel || outcome.symbol.type || '';
|
|
1659
1815
|
return this._runImpactBFS(repo, sym, symType, direction, {
|
|
1660
1816
|
maxDepth,
|
|
1661
1817
|
relationTypes,
|