sweet-search 2.5.2 → 2.5.4
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/core/cli.js +24 -3
- package/core/graph/graph-expansion.js +215 -36
- package/core/graph/graph-extractor.js +196 -11
- package/core/graph/graph-search.js +395 -92
- package/core/graph/hcgs-generator.js +2 -1
- package/core/graph/index.js +2 -0
- package/core/graph/repo-map.js +28 -6
- package/core/graph/structural-answer-cues.js +168 -0
- package/core/graph/structural-callsite-hints.js +40 -0
- package/core/graph/structural-context-format.js +40 -0
- package/core/graph/structural-context.js +450 -0
- package/core/graph/structural-forward-push.js +156 -0
- package/core/graph/structural-header-context.js +19 -0
- package/core/graph/structural-importance.js +148 -0
- package/core/graph/structural-pagerank.js +197 -0
- package/core/graph/summary-manager.js +13 -9
- package/core/incremental-indexing/application/dirty-scan.mjs +236 -0
- package/core/incremental-indexing/application/file-watcher.mjs +197 -0
- package/core/incremental-indexing/application/maintenance-handlers.mjs +519 -0
- package/core/incremental-indexing/application/maintenance-worker.mjs +380 -0
- package/core/incremental-indexing/application/operator-cli.mjs +554 -0
- package/core/incremental-indexing/application/production-li-delta.mjs +192 -0
- package/core/incremental-indexing/application/production-reconciler-helpers.mjs +107 -0
- package/core/incremental-indexing/application/production-reconciler.mjs +583 -0
- package/core/incremental-indexing/application/reconciler.mjs +477 -0
- package/core/incremental-indexing/application/tombstone-injector.mjs +148 -0
- package/core/incremental-indexing/domain/chunk-identity.mjs +260 -0
- package/core/incremental-indexing/domain/encoder-deps.mjs +193 -0
- package/core/incremental-indexing/domain/encoder-input.mjs +225 -0
- package/core/incremental-indexing/domain/interval-autotune.mjs +255 -0
- package/core/incremental-indexing/domain/reconcile-counters.mjs +149 -0
- package/core/incremental-indexing/domain/watermark-scheduler.mjs +239 -0
- package/core/incremental-indexing/infrastructure/artifact-temp-sweep.mjs +163 -0
- package/core/incremental-indexing/infrastructure/baseline-readiness.mjs +121 -0
- package/core/incremental-indexing/infrastructure/dirty-set.mjs +233 -0
- package/core/incremental-indexing/infrastructure/graph-gc.mjs +314 -0
- package/core/incremental-indexing/infrastructure/hashing.mjs +298 -0
- package/core/incremental-indexing/infrastructure/hcgs-invalidation.mjs +182 -0
- package/core/incremental-indexing/infrastructure/li-segment-merge.mjs +278 -0
- package/core/incremental-indexing/infrastructure/li-segment-state.mjs +173 -0
- package/core/incremental-indexing/infrastructure/lockfile.mjs +119 -0
- package/core/incremental-indexing/infrastructure/maintenance-state-reader.mjs +283 -0
- package/core/incremental-indexing/infrastructure/manifest.mjs +194 -0
- package/core/incremental-indexing/infrastructure/path-filter.mjs +190 -0
- package/core/incremental-indexing/infrastructure/reader-heartbeat.mjs +201 -0
- package/core/incremental-indexing/infrastructure/schema-migrations.mjs +257 -0
- package/core/incremental-indexing/infrastructure/sparse-gram-delta.mjs +335 -0
- package/core/incremental-indexing/infrastructure/sqlite-fts5.mjs +176 -0
- package/core/incremental-indexing/infrastructure/staleness-display.mjs +105 -0
- package/core/incremental-indexing/infrastructure/tombstone-bitmap.mjs +234 -0
- package/core/incremental-indexing/infrastructure/vector-delta-writer.mjs +359 -0
- package/core/incremental-indexing/infrastructure/vector-gc.mjs +133 -0
- package/core/incremental-indexing/infrastructure/worktree-stamp.mjs +155 -0
- package/core/incremental-indexing/infrastructure/wsl2-detect.mjs +115 -0
- package/core/indexing/admission-policy.js +139 -0
- package/core/indexing/artifact-builder.js +29 -12
- package/core/indexing/ast-chunker.js +107 -30
- package/core/indexing/dedup/exemplar-selector.js +19 -1
- package/core/indexing/gitignore-filter.js +223 -0
- package/core/indexing/incremental-tracker.js +99 -30
- package/core/indexing/index-codebase-v21.js +6 -5
- package/core/indexing/index-maintainer.mjs +698 -6
- package/core/indexing/indexer-ann.js +99 -15
- package/core/indexing/indexer-build.js +158 -45
- package/core/indexing/indexer-empty-baseline.js +80 -0
- package/core/indexing/indexer-manifest.js +66 -0
- package/core/indexing/indexer-phases.js +56 -23
- package/core/indexing/indexer-sparse-gram.js +54 -13
- package/core/indexing/indexer-utils.js +26 -208
- package/core/indexing/indexing-file-policy.js +32 -7
- package/core/indexing/maintainer-launcher.mjs +137 -0
- package/core/indexing/merkle-tracker.js +251 -244
- package/core/indexing/model-pool.js +46 -5
- package/core/infrastructure/code-graph-repository.js +758 -6
- package/core/infrastructure/code-graph-visibility.js +157 -0
- package/core/infrastructure/codebase-repository.js +100 -13
- package/core/infrastructure/config/search.js +1 -1
- package/core/infrastructure/db-utils.js +118 -0
- package/core/infrastructure/dedup-hashing.js +10 -13
- package/core/infrastructure/hardware-capability.js +17 -7
- package/core/infrastructure/index.js +8 -2
- package/core/infrastructure/language-patterns/maps.js +4 -1
- package/core/infrastructure/language-patterns/registry-core.js +56 -17
- package/core/infrastructure/language-patterns/registry-object-oriented.js +12 -5
- package/core/infrastructure/language-patterns.js +69 -0
- package/core/infrastructure/model-registry.js +20 -0
- package/core/infrastructure/native-inference.js +7 -12
- package/core/infrastructure/native-resolver.js +52 -37
- package/core/infrastructure/native-sparse-gram.js +261 -20
- package/core/infrastructure/native-tokenizer.js +6 -15
- package/core/infrastructure/simd-distance.js +10 -16
- package/core/infrastructure/sparse-gram-delta-reader.js +76 -0
- package/core/infrastructure/structural-alias-resolver.js +122 -0
- package/core/infrastructure/structural-candidate-ranker.js +34 -0
- package/core/infrastructure/structural-context-repository.js +472 -0
- package/core/infrastructure/structural-context-utils.js +51 -0
- package/core/infrastructure/structural-graph-signals.js +121 -0
- package/core/infrastructure/structural-qualified-resolution.js +15 -0
- package/core/infrastructure/structural-source-definitions.js +100 -0
- package/core/infrastructure/tombstone-bitmap-reader.js +139 -0
- package/core/infrastructure/tree-sitter-provider.js +811 -37
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +50 -0
- package/core/query/query-router.js +55 -5
- package/core/ranking/file-kind-ranking.js +2192 -15
- package/core/ranking/late-interaction-index.js +87 -12
- package/core/search/cli-decoration.js +290 -0
- package/core/search/context-expander.js +988 -78
- package/core/search/index.js +1 -0
- package/core/search/output-policy.js +275 -0
- package/core/search/search-anchor.js +499 -0
- package/core/search/search-boost.js +93 -1
- package/core/search/search-cli.js +61 -204
- package/core/search/search-hybrid.js +250 -10
- package/core/search/search-pattern-chunks.js +57 -8
- package/core/search/search-pattern-planner.js +68 -9
- package/core/search/search-pattern-prefilter.js +30 -10
- package/core/search/search-pattern-ripgrep.js +40 -4
- package/core/search/search-pattern-sparse-overlay.js +256 -0
- package/core/search/search-pattern.js +117 -29
- package/core/search/search-postprocess.js +479 -5
- package/core/search/search-read-semantic.js +260 -23
- package/core/search/search-read.js +82 -64
- package/core/search/search-reader-pin.js +71 -0
- package/core/search/search-rrf.js +279 -0
- package/core/search/search-semantic.js +110 -5
- package/core/search/search-server.js +130 -57
- package/core/search/search-trace.js +107 -0
- package/core/search/server-identity.js +93 -0
- package/core/search/session-daemon-prewarm.mjs +33 -10
- package/core/search/sweet-search.js +399 -7
- package/core/skills/sweet-index/SKILL.md +8 -6
- package/core/vector-store/binary-hnsw-index.js +194 -30
- package/core/vector-store/float-vector-store.js +96 -6
- package/core/vector-store/hnsw-index.js +220 -49
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +471 -0
- package/eval/agent-read-workflows/bin/ss-find +15 -0
- package/eval/agent-read-workflows/bin/ss-grep +12 -0
- package/eval/agent-read-workflows/bin/ss-read +14 -0
- package/eval/agent-read-workflows/bin/ss-search +18 -0
- package/eval/agent-read-workflows/bin/ss-semantic +12 -0
- package/eval/agent-read-workflows/bin/ss-trace +11 -0
- package/mcp/read-tool.js +109 -0
- package/mcp/server.js +55 -15
- package/mcp/tool-handlers.js +14 -124
- package/mcp/trace-tool.js +81 -0
- package/package.json +25 -10
- package/scripts/hooks/intercept-read.mjs +55 -0
- package/scripts/hooks/remind-tools.mjs +40 -0
- package/scripts/init.js +698 -54
- package/scripts/inject-agent-instructions.js +431 -0
- package/scripts/install-prompt-reminders.js +188 -0
- package/scripts/install-tool-enforcement.js +220 -0
- package/scripts/smoke-test.js +12 -9
- package/scripts/uninstall.js +276 -18
- package/scripts/write-claude-rules.js +110 -0
|
@@ -11,15 +11,58 @@
|
|
|
11
11
|
import Database from 'better-sqlite3';
|
|
12
12
|
import { existsSync, statSync } from 'fs';
|
|
13
13
|
import { applyReadPragmas } from './db-utils.js';
|
|
14
|
+
import { readAdjacentManifest, resolveManifestCodeGraphPath, sqlAliasPrefix } from './code-graph-visibility.js';
|
|
14
15
|
|
|
15
16
|
export class CodeGraphRepository {
|
|
16
|
-
constructor(dbPath) {
|
|
17
|
-
this.
|
|
17
|
+
constructor(dbPath, options = {}) {
|
|
18
|
+
this._baseDbPath = dbPath;
|
|
19
|
+
this._explicitManifestEpoch = Number.isInteger(options.manifestEpoch);
|
|
20
|
+
this._manifestEpoch = this._explicitManifestEpoch ? options.manifestEpoch : null;
|
|
21
|
+
const manifest = this._explicitManifestEpoch ? readAdjacentManifest(this._baseDbPath) : null;
|
|
22
|
+
this._dbPath = this._explicitManifestEpoch
|
|
23
|
+
? resolveManifestCodeGraphPath(this._baseDbPath, manifest)
|
|
24
|
+
: dbPath;
|
|
18
25
|
this._db = null;
|
|
26
|
+
this._hasEntityEpochVisibility = null;
|
|
27
|
+
this._hasRelationshipEpochVisibility = null;
|
|
28
|
+
if (!this._explicitManifestEpoch) {
|
|
29
|
+
this._syncAdjacentManifest();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_syncAdjacentManifest() {
|
|
34
|
+
if (this._explicitManifestEpoch) return false;
|
|
35
|
+
const manifest = readAdjacentManifest(this._baseDbPath);
|
|
36
|
+
const nextEpoch = Number.isInteger(manifest?.epoch) ? manifest.epoch : null;
|
|
37
|
+
const nextDbPath = resolveManifestCodeGraphPath(this._baseDbPath, manifest);
|
|
38
|
+
const changed = nextEpoch !== this._manifestEpoch || nextDbPath !== this._dbPath;
|
|
39
|
+
this._manifestEpoch = nextEpoch;
|
|
40
|
+
this._dbPath = nextDbPath;
|
|
41
|
+
if (changed) {
|
|
42
|
+
this.close();
|
|
43
|
+
}
|
|
44
|
+
return changed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
refreshManifestEpoch() {
|
|
48
|
+
this._syncAdjacentManifest();
|
|
49
|
+
return this._manifestEpoch;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getManifestEpoch() {
|
|
53
|
+
return this._manifestEpoch;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_resetDerivedCaches() {
|
|
57
|
+
this._refCountExact = null;
|
|
58
|
+
this._refSuffixSafe = null;
|
|
59
|
+
this._refBareRelationshipFanout = null;
|
|
60
|
+
this._refCountIndex = null;
|
|
19
61
|
}
|
|
20
62
|
|
|
21
63
|
/** Lazy read-only connection with optimized pragmas. */
|
|
22
64
|
_open() {
|
|
65
|
+
this._syncAdjacentManifest();
|
|
23
66
|
if (!this._db) {
|
|
24
67
|
if (!existsSync(this._dbPath)) return null;
|
|
25
68
|
this._db = new Database(this._dbPath, { readonly: true });
|
|
@@ -28,6 +71,56 @@ export class CodeGraphRepository {
|
|
|
28
71
|
return this._db;
|
|
29
72
|
}
|
|
30
73
|
|
|
74
|
+
_hasColumns(db, table, columns) {
|
|
75
|
+
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
76
|
+
const names = new Set(rows.map((row) => row.name));
|
|
77
|
+
return columns.every((column) => names.has(column));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_ensureVisibilityInfo(db) {
|
|
81
|
+
if (this._hasEntityEpochVisibility === null) {
|
|
82
|
+
this._hasEntityEpochVisibility = this._hasColumns(db, 'entities', ['epoch_written', 'epoch_retired']);
|
|
83
|
+
}
|
|
84
|
+
if (this._hasRelationshipEpochVisibility === null) {
|
|
85
|
+
this._hasRelationshipEpochVisibility = this._hasColumns(db, 'relationships', ['epoch_written', 'epoch_retired']);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
_entityVisibilitySql(db, alias = '') {
|
|
90
|
+
this._ensureVisibilityInfo(db);
|
|
91
|
+
const prefix = sqlAliasPrefix(alias);
|
|
92
|
+
if (!this._hasEntityEpochVisibility) return `${prefix}stale_since IS NULL`;
|
|
93
|
+
if (this._manifestEpoch !== null) {
|
|
94
|
+
return `(${prefix}epoch_written IS NULL OR ${prefix}epoch_written <= ?)
|
|
95
|
+
AND (${prefix}epoch_retired IS NULL OR ${prefix}epoch_retired > ?)
|
|
96
|
+
AND (${prefix}stale_since IS NULL OR (${prefix}epoch_retired IS NOT NULL AND ${prefix}epoch_retired > ?))`;
|
|
97
|
+
}
|
|
98
|
+
return `${prefix}stale_since IS NULL AND ${prefix}epoch_retired IS NULL`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_entityVisibilityParams(db) {
|
|
102
|
+
this._ensureVisibilityInfo(db);
|
|
103
|
+
if (!this._hasEntityEpochVisibility || this._manifestEpoch === null) return [];
|
|
104
|
+
return [this._manifestEpoch, this._manifestEpoch, this._manifestEpoch];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_relationshipVisibilitySql(db, alias = 'r') {
|
|
108
|
+
this._ensureVisibilityInfo(db);
|
|
109
|
+
if (!this._hasRelationshipEpochVisibility) return '1=1';
|
|
110
|
+
const prefix = sqlAliasPrefix(alias);
|
|
111
|
+
if (this._manifestEpoch !== null) {
|
|
112
|
+
return `(${prefix}epoch_written IS NULL OR ${prefix}epoch_written <= ?)
|
|
113
|
+
AND (${prefix}epoch_retired IS NULL OR ${prefix}epoch_retired > ?)`;
|
|
114
|
+
}
|
|
115
|
+
return `${prefix}epoch_retired IS NULL`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_relationshipVisibilityParams(db) {
|
|
119
|
+
this._ensureVisibilityInfo(db);
|
|
120
|
+
if (!this._hasRelationshipEpochVisibility || this._manifestEpoch === null) return [];
|
|
121
|
+
return [this._manifestEpoch, this._manifestEpoch];
|
|
122
|
+
}
|
|
123
|
+
|
|
31
124
|
/**
|
|
32
125
|
* Find the tightest entity that fully encloses a given line range.
|
|
33
126
|
*
|
|
@@ -39,34 +132,658 @@ export class CodeGraphRepository {
|
|
|
39
132
|
* @param {string} filePath - Relative file path (as stored in entities.file_path)
|
|
40
133
|
* @param {number} startLine - 1-indexed start line of the chunk
|
|
41
134
|
* @param {number} endLine - 1-indexed end line of the chunk
|
|
42
|
-
* @returns {{ name: string, type: string, startLine: number, endLine: number, parentClass: string|null }|null}
|
|
135
|
+
* @returns {{ id: string, name: string, type: string, startLine: number, endLine: number, parentClass: string|null }|null}
|
|
43
136
|
*/
|
|
44
137
|
findEnclosingEntity(filePath, startLine, endLine) {
|
|
45
138
|
const db = this._open();
|
|
46
139
|
if (!db) return null;
|
|
47
140
|
try {
|
|
48
141
|
const row = db.prepare(`
|
|
49
|
-
SELECT name, type, start_line, end_line, parent_class
|
|
142
|
+
SELECT id, name, type, start_line, end_line, parent_class
|
|
50
143
|
FROM entities
|
|
51
144
|
WHERE file_path = ?
|
|
52
145
|
AND start_line <= ?
|
|
53
146
|
AND end_line >= ?
|
|
147
|
+
AND ${this._entityVisibilitySql(db)}
|
|
54
148
|
ORDER BY (end_line - start_line) ASC
|
|
55
149
|
LIMIT 1
|
|
56
|
-
`).get(filePath, startLine, endLine);
|
|
150
|
+
`).get(filePath, startLine, endLine, ...this._entityVisibilityParams(db));
|
|
151
|
+
if (!row) return null;
|
|
152
|
+
return {
|
|
153
|
+
id: row.id,
|
|
154
|
+
name: row.name,
|
|
155
|
+
type: row.type,
|
|
156
|
+
startLine: row.start_line,
|
|
157
|
+
endLine: row.end_line,
|
|
158
|
+
parentClass: row.parent_class || null,
|
|
159
|
+
};
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Find the first indexed entity fully contained in a chunk range.
|
|
167
|
+
* Useful when a chunk starts at file line 1 but includes the declaration
|
|
168
|
+
* shortly after imports/comments, so strict enclosing lookup cannot match.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} filePath
|
|
171
|
+
* @param {number} startLine
|
|
172
|
+
* @param {number} endLine
|
|
173
|
+
* @returns {{ id: string, name: string, type: string, startLine: number, endLine: number, parentClass: string|null }|null}
|
|
174
|
+
*/
|
|
175
|
+
/**
|
|
176
|
+
* Check whether a chunk range contains an entity whose name matches the target
|
|
177
|
+
* (case-insensitive, also tries snake_case ↔ camelCase normalization).
|
|
178
|
+
* Used by symbol-exact-match boost when chunk's labeled symbol doesn't match
|
|
179
|
+
* but a sibling/contained entity does.
|
|
180
|
+
*
|
|
181
|
+
* @param {string} filePath
|
|
182
|
+
* @param {number} startLine
|
|
183
|
+
* @param {number} endLine
|
|
184
|
+
* @param {string} targetName
|
|
185
|
+
* @returns {boolean}
|
|
186
|
+
*/
|
|
187
|
+
/**
|
|
188
|
+
* Find the matching entity by name within a chunk range, if it exists.
|
|
189
|
+
* Returns the actual canonical name + type (so callers can relabel chunks
|
|
190
|
+
* to the contained-entity name when the query target matches).
|
|
191
|
+
*
|
|
192
|
+
* @param {string} filePath
|
|
193
|
+
* @param {number} startLine
|
|
194
|
+
* @param {number} endLine
|
|
195
|
+
* @param {string} targetName
|
|
196
|
+
* @returns {{ name: string, type: string, startLine: number, endLine: number }|null}
|
|
197
|
+
*/
|
|
198
|
+
findEntityWithNameInRange(filePath, startLine, endLine, targetName) {
|
|
199
|
+
if (!targetName) return null;
|
|
200
|
+
const db = this._open();
|
|
201
|
+
if (!db) return null;
|
|
202
|
+
try {
|
|
203
|
+
const tLower = String(targetName).toLowerCase();
|
|
204
|
+
const tNorm = tLower.replace(/[_-]/g, '');
|
|
205
|
+
// Two-pass query: prefer entities FULLY CONTAINED in chunk range (most
|
|
206
|
+
// confident match); if none, fall back to entities whose START line is
|
|
207
|
+
// in the chunk range (catches entities like a multi-chunk struct whose
|
|
208
|
+
// declaration starts in this chunk but body extends beyond — e.g. gin
|
|
209
|
+
// Engine struct at gin.go:92-189 split across chunks 92-133 and 134-189).
|
|
210
|
+
const containedRows = db.prepare(`
|
|
211
|
+
SELECT name, type, start_line, end_line FROM entities
|
|
212
|
+
WHERE file_path = ? AND start_line >= ? AND end_line <= ? AND ${this._entityVisibilitySql(db)}
|
|
213
|
+
ORDER BY (end_line - start_line) DESC
|
|
214
|
+
`).all(filePath, startLine, endLine, ...this._entityVisibilityParams(db));
|
|
215
|
+
for (const row of containedRows) {
|
|
216
|
+
if (!row.name) continue;
|
|
217
|
+
const nLower = String(row.name).toLowerCase();
|
|
218
|
+
if (nLower === tLower || nLower.replace(/[_-]/g, '') === tNorm) {
|
|
219
|
+
return { name: row.name, type: row.type, startLine: row.start_line, endLine: row.end_line };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const startsInRows = db.prepare(`
|
|
223
|
+
SELECT name, type, start_line, end_line FROM entities
|
|
224
|
+
WHERE file_path = ? AND start_line >= ? AND start_line <= ? AND ${this._entityVisibilitySql(db)}
|
|
225
|
+
ORDER BY (end_line - start_line) DESC
|
|
226
|
+
`).all(filePath, startLine, endLine, ...this._entityVisibilityParams(db));
|
|
227
|
+
for (const row of startsInRows) {
|
|
228
|
+
if (!row.name) continue;
|
|
229
|
+
const nLower = String(row.name).toLowerCase();
|
|
230
|
+
if (nLower === tLower || nLower.replace(/[_-]/g, '') === tNorm) {
|
|
231
|
+
return { name: row.name, type: row.type, startLine: row.start_line, endLine: row.end_line };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return null;
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
hasEntityWithNameInRange(filePath, startLine, endLine, targetName) {
|
|
241
|
+
return this.findEntityWithNameInRange(filePath, startLine, endLine, targetName) !== null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
findFirstEntityInRange(filePath, startLine, endLine) {
|
|
245
|
+
const db = this._open();
|
|
246
|
+
if (!db) return null;
|
|
247
|
+
try {
|
|
248
|
+
const row = db.prepare(`
|
|
249
|
+
SELECT id, name, type, start_line, end_line, parent_class
|
|
250
|
+
FROM entities
|
|
251
|
+
WHERE file_path = ?
|
|
252
|
+
AND start_line >= ?
|
|
253
|
+
AND start_line <= ?
|
|
254
|
+
AND ${this._entityVisibilitySql(db)}
|
|
255
|
+
ORDER BY start_line ASC, (end_line - start_line) ASC
|
|
256
|
+
LIMIT 1
|
|
257
|
+
`).get(filePath, startLine, endLine, ...this._entityVisibilityParams(db));
|
|
57
258
|
if (!row) return null;
|
|
58
259
|
return {
|
|
260
|
+
id: row.id,
|
|
261
|
+
name: row.name,
|
|
262
|
+
type: row.type,
|
|
263
|
+
startLine: row.start_line,
|
|
264
|
+
endLine: row.end_line,
|
|
265
|
+
parentClass: row.parent_class || null,
|
|
266
|
+
};
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Return ALL entities whose start_line falls in [startLine, endLine]
|
|
274
|
+
* (i.e. every entity declared inside the chunk's range). Used by the
|
|
275
|
+
* F9 additional_symbols re-anchoring rule (file-kind-ranking.js): when a
|
|
276
|
+
* chunk merged ≥2 top-level boundaries, the chunker's stored `symbol` is
|
|
277
|
+
* just the first boundary's name. Re-querying the entities table picks up
|
|
278
|
+
* every declared symbol in the chunk and lets ranking pick the best name
|
|
279
|
+
* match against the user's query.
|
|
280
|
+
*
|
|
281
|
+
* Capped at 64 to bound the per-result scan cost. Ordered by start_line.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} filePath
|
|
284
|
+
* @param {number} startLine
|
|
285
|
+
* @param {number} endLine
|
|
286
|
+
* @returns {Array<{ id, name, type, startLine, endLine, parentClass }>}
|
|
287
|
+
*/
|
|
288
|
+
findEntitiesInRange(filePath, startLine, endLine) {
|
|
289
|
+
const db = this._open();
|
|
290
|
+
if (!db) return [];
|
|
291
|
+
try {
|
|
292
|
+
const rows = db.prepare(`
|
|
293
|
+
SELECT id, name, type, start_line, end_line, parent_class
|
|
294
|
+
FROM entities
|
|
295
|
+
WHERE file_path = ?
|
|
296
|
+
AND start_line >= ?
|
|
297
|
+
AND start_line <= ?
|
|
298
|
+
AND ${this._entityVisibilitySql(db)}
|
|
299
|
+
ORDER BY start_line ASC
|
|
300
|
+
LIMIT 64
|
|
301
|
+
`).all(filePath, startLine, endLine, ...this._entityVisibilityParams(db));
|
|
302
|
+
return rows.map(row => ({
|
|
303
|
+
id: row.id,
|
|
59
304
|
name: row.name,
|
|
60
305
|
type: row.type,
|
|
61
306
|
startLine: row.start_line,
|
|
62
307
|
endLine: row.end_line,
|
|
63
308
|
parentClass: row.parent_class || null,
|
|
309
|
+
}));
|
|
310
|
+
} catch {
|
|
311
|
+
return [];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get a single entity by id, with file:line metadata.
|
|
317
|
+
*
|
|
318
|
+
* @param {string} entityId
|
|
319
|
+
* @returns {{ id, name, type, filePath, startLine, endLine, parentClass }|null}
|
|
320
|
+
*/
|
|
321
|
+
getEntityById(entityId) {
|
|
322
|
+
const db = this._open();
|
|
323
|
+
if (!db) return null;
|
|
324
|
+
try {
|
|
325
|
+
const row = db.prepare(`
|
|
326
|
+
SELECT id, name, type, file_path, start_line, end_line, parent_class
|
|
327
|
+
FROM entities
|
|
328
|
+
WHERE id = ? AND ${this._entityVisibilitySql(db)}
|
|
329
|
+
`).get(entityId, ...this._entityVisibilityParams(db));
|
|
330
|
+
if (!row) return null;
|
|
331
|
+
return {
|
|
332
|
+
id: row.id, name: row.name, type: row.type,
|
|
333
|
+
filePath: row.file_path,
|
|
334
|
+
startLine: row.start_line, endLine: row.end_line,
|
|
335
|
+
parentClass: row.parent_class || null,
|
|
64
336
|
};
|
|
65
337
|
} catch {
|
|
66
338
|
return null;
|
|
67
339
|
}
|
|
68
340
|
}
|
|
69
341
|
|
|
342
|
+
/**
|
|
343
|
+
* One-hop outgoing relationships from a given source entity.
|
|
344
|
+
*
|
|
345
|
+
* Returns up to `limit` (target_name, type, context_line, full_import_path)
|
|
346
|
+
* tuples joined to the target entity's metadata when target_id is resolved.
|
|
347
|
+
* Used by the agent context packager to render a "neighbours" tier.
|
|
348
|
+
*
|
|
349
|
+
* The relationship `type` field comes from graph-extractor and is one of:
|
|
350
|
+
* imports | calls | extends | implements | overrides | throws | uses
|
|
351
|
+
*
|
|
352
|
+
* @param {string} sourceId
|
|
353
|
+
* @param {object} [opts]
|
|
354
|
+
* @param {string[]} [opts.types] - filter to these types (default: all)
|
|
355
|
+
* @param {number} [opts.limit=8]
|
|
356
|
+
* @returns {Array<{ type, targetName, targetId: string|null, contextLine: number|null,
|
|
357
|
+
* fullImportPath: string|null, target: { id, name, type, filePath, startLine, endLine }|null }>}
|
|
358
|
+
*/
|
|
359
|
+
getOutgoingRelationships(sourceId, opts = {}) {
|
|
360
|
+
const db = this._open();
|
|
361
|
+
if (!db || !sourceId) return [];
|
|
362
|
+
const limit = Math.max(1, Math.min(50, opts.limit ?? 8));
|
|
363
|
+
const types = (opts.types && opts.types.length) ? opts.types : null;
|
|
364
|
+
try {
|
|
365
|
+
const baseSql = `
|
|
366
|
+
SELECT r.target_id, r.target_name, r.type as rel_type, r.context_line,
|
|
367
|
+
r.full_import_path,
|
|
368
|
+
e.id as e_id, e.name as e_name, e.type as e_type,
|
|
369
|
+
e.file_path as e_file, e.start_line as e_start, e.end_line as e_end
|
|
370
|
+
FROM relationships r
|
|
371
|
+
LEFT JOIN entities e ON e.id = r.target_id AND ${this._entityVisibilitySql(db, 'e')}
|
|
372
|
+
WHERE r.source_id = ?
|
|
373
|
+
AND ${this._relationshipVisibilitySql(db, 'r')}
|
|
374
|
+
AND (r.target_id IS NULL OR e.id IS NOT NULL)
|
|
375
|
+
${types ? `AND r.type IN (${types.map(() => '?').join(',')})` : ''}
|
|
376
|
+
ORDER BY (CASE WHEN r.target_id IS NULL THEN 1 ELSE 0 END), r.weight DESC
|
|
377
|
+
LIMIT ?
|
|
378
|
+
`;
|
|
379
|
+
const args = types
|
|
380
|
+
? [...this._entityVisibilityParams(db), sourceId, ...this._relationshipVisibilityParams(db), ...types, limit]
|
|
381
|
+
: [...this._entityVisibilityParams(db), sourceId, ...this._relationshipVisibilityParams(db), limit];
|
|
382
|
+
const rows = db.prepare(baseSql).all(...args);
|
|
383
|
+
return rows.map(r => ({
|
|
384
|
+
type: r.rel_type,
|
|
385
|
+
targetName: r.target_name,
|
|
386
|
+
targetId: r.target_id || null,
|
|
387
|
+
contextLine: r.context_line || null,
|
|
388
|
+
fullImportPath: r.full_import_path || null,
|
|
389
|
+
target: r.e_id ? {
|
|
390
|
+
id: r.e_id, name: r.e_name, type: r.e_type,
|
|
391
|
+
filePath: r.e_file, startLine: r.e_start, endLine: r.e_end,
|
|
392
|
+
} : null,
|
|
393
|
+
}));
|
|
394
|
+
} catch {
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Look up entities by name (case-sensitive), filtered to a set of types.
|
|
401
|
+
*
|
|
402
|
+
* Used by the agent context packager to surface TYPE definitions
|
|
403
|
+
* (struct/enum/interface/class/trait/type) referenced from the top-1's
|
|
404
|
+
* body — the relationships table only captures explicit edges (calls,
|
|
405
|
+
* imports), so a method that uses a struct via a field's type
|
|
406
|
+
* declaration leaves no edge. This name-based lookup recovers them.
|
|
407
|
+
*
|
|
408
|
+
* Returns at most one entity per (name, type) pair, preferring
|
|
409
|
+
* non-stale entries with the smallest body (most likely the canonical
|
|
410
|
+
* definition rather than a re-export).
|
|
411
|
+
*
|
|
412
|
+
* @param {string[]} names - candidate identifier names (Capitalized, etc.)
|
|
413
|
+
* @param {object} [opts]
|
|
414
|
+
* @param {string[]} [opts.types] - filter to entity types
|
|
415
|
+
* (default: ['struct','class','interface','enum','trait','type'])
|
|
416
|
+
* @param {number} [opts.limit=8] - cap total returned entities
|
|
417
|
+
* @param {string} [opts.excludeFile] - skip entities defined in this file
|
|
418
|
+
* (caller's own file, since same-file ranks already cover that)
|
|
419
|
+
* @returns {Array<{ id, name, type, filePath, startLine, endLine }>}
|
|
420
|
+
*/
|
|
421
|
+
findEntitiesByNames(names, opts = {}) {
|
|
422
|
+
const db = this._open();
|
|
423
|
+
if (!db || !Array.isArray(names) || names.length === 0) return [];
|
|
424
|
+
const uniq = [...new Set(names.filter(n => typeof n === 'string' && n.length >= 2))];
|
|
425
|
+
if (!uniq.length) return [];
|
|
426
|
+
const types = (opts.types && opts.types.length)
|
|
427
|
+
? opts.types
|
|
428
|
+
: ['struct', 'class', 'interface', 'enum', 'trait', 'type', 'typeAlias'];
|
|
429
|
+
const limit = Math.max(1, Math.min(32, opts.limit ?? 8));
|
|
430
|
+
const excludeFile = typeof opts.excludeFile === 'string' ? opts.excludeFile : null;
|
|
431
|
+
try {
|
|
432
|
+
// One row per (name, type), picking the smallest body when name collides
|
|
433
|
+
// (canonical definition rather than re-export). chunk-style entities are
|
|
434
|
+
// intentionally excluded to avoid false hits on test scaffolding.
|
|
435
|
+
const sql = `
|
|
436
|
+
SELECT id, name, type, file_path, start_line, end_line
|
|
437
|
+
FROM entities
|
|
438
|
+
WHERE name IN (${uniq.map(() => '?').join(',')})
|
|
439
|
+
AND type IN (${types.map(() => '?').join(',')})
|
|
440
|
+
AND ${this._entityVisibilitySql(db)}
|
|
441
|
+
${excludeFile ? 'AND file_path != ?' : ''}
|
|
442
|
+
ORDER BY (end_line - start_line) ASC
|
|
443
|
+
LIMIT ?
|
|
444
|
+
`;
|
|
445
|
+
const args = excludeFile
|
|
446
|
+
? [...uniq, ...types, ...this._entityVisibilityParams(db), excludeFile, limit]
|
|
447
|
+
: [...uniq, ...types, ...this._entityVisibilityParams(db), limit];
|
|
448
|
+
const rows = db.prepare(sql).all(...args);
|
|
449
|
+
// De-dup by name+type, keeping the first (smallest body).
|
|
450
|
+
const seen = new Set();
|
|
451
|
+
const out = [];
|
|
452
|
+
for (const r of rows) {
|
|
453
|
+
const k = `${r.name}|${r.type}`;
|
|
454
|
+
if (seen.has(k)) continue;
|
|
455
|
+
seen.add(k);
|
|
456
|
+
out.push({
|
|
457
|
+
id: r.id, name: r.name, type: r.type,
|
|
458
|
+
filePath: r.file_path, startLine: r.start_line, endLine: r.end_line,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
return out;
|
|
462
|
+
} catch {
|
|
463
|
+
return [];
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Case-insensitive variant for query-derived name hints. Used only for
|
|
469
|
+
* ranking tiebreakers where user prose may lowercase an entity name.
|
|
470
|
+
*
|
|
471
|
+
* @param {string[]} names
|
|
472
|
+
* @param {object} [opts]
|
|
473
|
+
* @returns {Array<{ id, name, type, filePath, startLine, endLine }>}
|
|
474
|
+
*/
|
|
475
|
+
findEntitiesByNamesCaseInsensitive(names, opts = {}) {
|
|
476
|
+
const db = this._open();
|
|
477
|
+
if (!db || !Array.isArray(names) || names.length === 0) return [];
|
|
478
|
+
const uniq = [...new Set(names
|
|
479
|
+
.filter(n => typeof n === 'string' && n.length >= 2)
|
|
480
|
+
.map(n => n.toLowerCase()))];
|
|
481
|
+
if (!uniq.length) return [];
|
|
482
|
+
const types = (opts.types && opts.types.length)
|
|
483
|
+
? opts.types
|
|
484
|
+
: ['struct', 'class', 'interface', 'enum', 'trait', 'type', 'typeAlias'];
|
|
485
|
+
const limit = Math.max(1, Math.min(32, opts.limit ?? 8));
|
|
486
|
+
try {
|
|
487
|
+
const sql = `
|
|
488
|
+
SELECT id, name, type, file_path, start_line, end_line
|
|
489
|
+
FROM entities
|
|
490
|
+
WHERE lower(name) IN (${uniq.map(() => '?').join(',')})
|
|
491
|
+
AND type IN (${types.map(() => '?').join(',')})
|
|
492
|
+
AND ${this._entityVisibilitySql(db)}
|
|
493
|
+
ORDER BY (end_line - start_line) ASC
|
|
494
|
+
LIMIT ?
|
|
495
|
+
`;
|
|
496
|
+
const rows = db.prepare(sql).all(...uniq, ...types, ...this._entityVisibilityParams(db), limit);
|
|
497
|
+
return rows.map(row => ({
|
|
498
|
+
id: row.id,
|
|
499
|
+
name: row.name,
|
|
500
|
+
type: row.type,
|
|
501
|
+
filePath: row.file_path,
|
|
502
|
+
startLine: row.start_line,
|
|
503
|
+
endLine: row.end_line,
|
|
504
|
+
}));
|
|
505
|
+
} catch {
|
|
506
|
+
return [];
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Look up entities by name (case-insensitive) across ALL entity kinds —
|
|
512
|
+
* functions, methods, type aliases, structs, classes, etc. Used by the
|
|
513
|
+
* Identifier-Anchored Retrieval (IAR) layer in search-anchor.js, which
|
|
514
|
+
* extracts identifier-shaped tokens from natural-language queries and
|
|
515
|
+
* needs to find matching entities regardless of their declared kind.
|
|
516
|
+
*
|
|
517
|
+
* Distinct from `findEntitiesByNamesCaseInsensitive` (which filters to
|
|
518
|
+
* type-shaped kinds for the entity-kind ranking preference).
|
|
519
|
+
*
|
|
520
|
+
* Returns a small set per name, preferring the smallest body (canonical
|
|
521
|
+
* definition over re-exports). Excludes obviously-non-symbol kinds
|
|
522
|
+
* ('chunk', 'message', 'topKey', 'target', 'variable') so we don't
|
|
523
|
+
* surface generic constants on hits like "config".
|
|
524
|
+
*
|
|
525
|
+
* @param {string[]} names
|
|
526
|
+
* @param {object} [opts]
|
|
527
|
+
* @param {number} [opts.limit=16]
|
|
528
|
+
* @param {string[]} [opts.excludeKinds]
|
|
529
|
+
* @returns {Array<{ id, name, type, filePath, startLine, endLine }>}
|
|
530
|
+
*/
|
|
531
|
+
findEntitiesByAnyName(names, opts = {}) {
|
|
532
|
+
const db = this._open();
|
|
533
|
+
if (!db || !Array.isArray(names) || names.length === 0) return [];
|
|
534
|
+
const uniq = [...new Set(names
|
|
535
|
+
.filter(n => typeof n === 'string' && n.length >= 2)
|
|
536
|
+
.map(n => n.toLowerCase()))];
|
|
537
|
+
if (!uniq.length) return [];
|
|
538
|
+
const exclude = Array.isArray(opts.excludeKinds) && opts.excludeKinds.length
|
|
539
|
+
? opts.excludeKinds
|
|
540
|
+
: ['chunk', 'message', 'topKey', 'target', 'variable', 'const'];
|
|
541
|
+
const limit = Math.max(1, Math.min(64, opts.limit ?? 16));
|
|
542
|
+
try {
|
|
543
|
+
const sql = `
|
|
544
|
+
SELECT id, name, type, file_path, start_line, end_line
|
|
545
|
+
FROM entities
|
|
546
|
+
WHERE lower(name) IN (${uniq.map(() => '?').join(',')})
|
|
547
|
+
AND type NOT IN (${exclude.map(() => '?').join(',')})
|
|
548
|
+
AND ${this._entityVisibilitySql(db)}
|
|
549
|
+
ORDER BY (end_line - start_line) ASC
|
|
550
|
+
LIMIT ?
|
|
551
|
+
`;
|
|
552
|
+
const rows = db.prepare(sql).all(...uniq, ...exclude, ...this._entityVisibilityParams(db), limit);
|
|
553
|
+
return rows.map(row => ({
|
|
554
|
+
id: row.id,
|
|
555
|
+
name: row.name,
|
|
556
|
+
type: row.type,
|
|
557
|
+
filePath: row.file_path,
|
|
558
|
+
startLine: row.start_line,
|
|
559
|
+
endLine: row.end_line,
|
|
560
|
+
}));
|
|
561
|
+
} catch {
|
|
562
|
+
return [];
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Count how many entities exist for each requested name (case-insensitive),
|
|
568
|
+
* across the same kind-set as `findEntitiesByAnyName`. Used by IAR to gate
|
|
569
|
+
* anchor injection on per-name uniqueness — common identifiers ("get",
|
|
570
|
+
* "set", "default") matching dozens of unrelated entities flood the
|
|
571
|
+
* candidate set; rare ones ("kSchemaController", "FST_ERR_HOOK_TIMEOUT")
|
|
572
|
+
* are safe.
|
|
573
|
+
*
|
|
574
|
+
* Mirrors KPR/SPAR's IDF-gated entity injection: the helpfulness of
|
|
575
|
+
* anchoring is proportional to entity rarity in the target corpus.
|
|
576
|
+
*
|
|
577
|
+
* @param {string[]} names
|
|
578
|
+
* @param {object} [opts]
|
|
579
|
+
* @param {string[]} [opts.excludeKinds]
|
|
580
|
+
* @returns {Map<string, number>} lowercase-name → entity count
|
|
581
|
+
*/
|
|
582
|
+
countEntitiesByAnyName(names, opts = {}) {
|
|
583
|
+
const db = this._open();
|
|
584
|
+
if (!db || !Array.isArray(names) || names.length === 0) return new Map();
|
|
585
|
+
const uniq = [...new Set(names
|
|
586
|
+
.filter(n => typeof n === 'string' && n.length >= 2)
|
|
587
|
+
.map(n => n.toLowerCase()))];
|
|
588
|
+
if (!uniq.length) return new Map();
|
|
589
|
+
const exclude = Array.isArray(opts.excludeKinds) && opts.excludeKinds.length
|
|
590
|
+
? opts.excludeKinds
|
|
591
|
+
: ['chunk', 'message', 'topKey', 'target', 'variable', 'const'];
|
|
592
|
+
try {
|
|
593
|
+
const sql = `
|
|
594
|
+
SELECT lower(name) as lname, COUNT(*) as count
|
|
595
|
+
FROM entities
|
|
596
|
+
WHERE lower(name) IN (${uniq.map(() => '?').join(',')})
|
|
597
|
+
AND type NOT IN (${exclude.map(() => '?').join(',')})
|
|
598
|
+
AND ${this._entityVisibilitySql(db)}
|
|
599
|
+
GROUP BY lname
|
|
600
|
+
`;
|
|
601
|
+
const rows = db.prepare(sql).all(...uniq, ...exclude, ...this._entityVisibilityParams(db));
|
|
602
|
+
const map = new Map();
|
|
603
|
+
for (const r of rows) map.set(r.lname, r.count);
|
|
604
|
+
// Names with no row are absent — caller treats absent as 0.
|
|
605
|
+
return map;
|
|
606
|
+
} catch {
|
|
607
|
+
return new Map();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* One-hop incoming relationships into a given target entity (its callers,
|
|
613
|
+
* importers, etc.). Joined to the source entity for file:line rendering.
|
|
614
|
+
*
|
|
615
|
+
* @param {string} targetId
|
|
616
|
+
* @param {object} [opts]
|
|
617
|
+
* @param {string[]} [opts.types]
|
|
618
|
+
* @param {number} [opts.limit=8]
|
|
619
|
+
* @returns {Array<{ type, source: { id, name, type, filePath, startLine, endLine }|null, contextLine }>}
|
|
620
|
+
*/
|
|
621
|
+
getIncomingRelationships(targetId, opts = {}) {
|
|
622
|
+
const db = this._open();
|
|
623
|
+
if (!db || !targetId) return [];
|
|
624
|
+
const limit = Math.max(1, Math.min(50, opts.limit ?? 8));
|
|
625
|
+
const types = (opts.types && opts.types.length) ? opts.types : null;
|
|
626
|
+
try {
|
|
627
|
+
const baseSql = `
|
|
628
|
+
SELECT r.source_id, r.type as rel_type, r.context_line,
|
|
629
|
+
e.id as e_id, e.name as e_name, e.type as e_type,
|
|
630
|
+
e.file_path as e_file, e.start_line as e_start, e.end_line as e_end
|
|
631
|
+
FROM relationships r
|
|
632
|
+
LEFT JOIN entities e ON e.id = r.source_id AND ${this._entityVisibilitySql(db, 'e')}
|
|
633
|
+
WHERE r.target_id = ?
|
|
634
|
+
AND ${this._relationshipVisibilitySql(db, 'r')}
|
|
635
|
+
AND e.id IS NOT NULL
|
|
636
|
+
${types ? `AND r.type IN (${types.map(() => '?').join(',')})` : ''}
|
|
637
|
+
ORDER BY r.weight DESC
|
|
638
|
+
LIMIT ?
|
|
639
|
+
`;
|
|
640
|
+
const args = types
|
|
641
|
+
? [...this._entityVisibilityParams(db), targetId, ...this._relationshipVisibilityParams(db), ...types, limit]
|
|
642
|
+
: [...this._entityVisibilityParams(db), targetId, ...this._relationshipVisibilityParams(db), limit];
|
|
643
|
+
const rows = db.prepare(baseSql).all(...args);
|
|
644
|
+
return rows.map(r => ({
|
|
645
|
+
type: r.rel_type,
|
|
646
|
+
contextLine: r.context_line || null,
|
|
647
|
+
source: r.e_id ? {
|
|
648
|
+
id: r.e_id, name: r.e_name, type: r.e_type,
|
|
649
|
+
filePath: r.e_file, startLine: r.e_start, endLine: r.e_end,
|
|
650
|
+
} : null,
|
|
651
|
+
})).filter(r => r.source);
|
|
652
|
+
} catch {
|
|
653
|
+
return [];
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Build in-memory ref-count indexes for incoming `calls` edges.
|
|
659
|
+
*
|
|
660
|
+
* Two maps:
|
|
661
|
+
* - `_refCountExact`: `target_name` → count (one GROUP BY row each)
|
|
662
|
+
* - `_refSuffixSafe`: bare suffix → Σ counts, only for bare names whose
|
|
663
|
+
* relationship fanout (distinct qualified targets sharing that suffix)
|
|
664
|
+
* is ≤ SWEET_SEARCH_REF_SUFFIX_AGG_FANOUT_MAX (default 12; tighten via env).
|
|
665
|
+
*
|
|
666
|
+
* Previously we always merged every `*.decorate` into bare `decorate`, which
|
|
667
|
+
* is correct for real repos (few homonyms) but poisons GenCodeSearchNet-style
|
|
668
|
+
* corpora (dozens of unrelated `get` functions sharing one inflated total).
|
|
669
|
+
*
|
|
670
|
+
* Lookup rule (see countIncomingCallsByNames):
|
|
671
|
+
* - Qualified names (contain `.`) → exact map only
|
|
672
|
+
* - Bare names → suffix safe aggregate when present, else exact fallback
|
|
673
|
+
*/
|
|
674
|
+
prebuildRefCountIndex() {
|
|
675
|
+
if (this._refCountExact) return this._refCountExact;
|
|
676
|
+
const exact = new Map();
|
|
677
|
+
const bareFanout = new Map();
|
|
678
|
+
const db = this._open();
|
|
679
|
+
if (!db) {
|
|
680
|
+
this._refCountExact = exact;
|
|
681
|
+
this._refSuffixSafe = new Map();
|
|
682
|
+
this._refBareRelationshipFanout = bareFanout;
|
|
683
|
+
this._refCountIndex = exact;
|
|
684
|
+
return exact;
|
|
685
|
+
}
|
|
686
|
+
const maxFan = (() => {
|
|
687
|
+
const raw = process.env.SWEET_SEARCH_REF_SUFFIX_AGG_FANOUT_MAX;
|
|
688
|
+
const n = parseInt(raw != null && raw !== '' ? raw : '12', 10);
|
|
689
|
+
return Number.isFinite(n) && n > 0 ? n : 12;
|
|
690
|
+
})();
|
|
691
|
+
try {
|
|
692
|
+
const rows = db.prepare(`
|
|
693
|
+
SELECT r.target_name, COUNT(*) as cnt
|
|
694
|
+
FROM relationships r
|
|
695
|
+
LEFT JOIN entities e ON e.id = r.target_id AND ${this._entityVisibilitySql(db, 'e')}
|
|
696
|
+
WHERE r.type = 'calls' AND r.target_name IS NOT NULL AND r.target_name != ''
|
|
697
|
+
AND ${this._relationshipVisibilitySql(db, 'r')}
|
|
698
|
+
AND (r.target_id IS NULL OR e.id IS NOT NULL)
|
|
699
|
+
GROUP BY target_name
|
|
700
|
+
`).all(...this._entityVisibilityParams(db), ...this._relationshipVisibilityParams(db));
|
|
701
|
+
|
|
702
|
+
for (const row of rows) {
|
|
703
|
+
const tn = row.target_name;
|
|
704
|
+
const cnt = row.cnt;
|
|
705
|
+
exact.set(tn, (exact.get(tn) || 0) + cnt);
|
|
706
|
+
const dot = tn.lastIndexOf('.');
|
|
707
|
+
const bareKey = dot > 0 ? tn.slice(dot + 1) : tn;
|
|
708
|
+
bareFanout.set(bareKey, (bareFanout.get(bareKey) || 0) + 1);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const suffix = new Map();
|
|
712
|
+
for (const row of rows) {
|
|
713
|
+
const tn = row.target_name;
|
|
714
|
+
const cnt = row.cnt;
|
|
715
|
+
const dot = tn.lastIndexOf('.');
|
|
716
|
+
const bareKey = dot > 0 ? tn.slice(dot + 1) : tn;
|
|
717
|
+
if ((bareFanout.get(bareKey) || 0) <= maxFan) {
|
|
718
|
+
suffix.set(bareKey, (suffix.get(bareKey) || 0) + cnt);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
this._refSuffixSafe = suffix;
|
|
723
|
+
} catch {
|
|
724
|
+
this._refSuffixSafe = new Map();
|
|
725
|
+
}
|
|
726
|
+
this._refCountExact = exact;
|
|
727
|
+
this._refBareRelationshipFanout = bareFanout;
|
|
728
|
+
// Legacy field: kept for any code that peeked at the exact map
|
|
729
|
+
this._refCountIndex = exact;
|
|
730
|
+
return exact;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* @param {string} bareName
|
|
735
|
+
* @returns {number}
|
|
736
|
+
*/
|
|
737
|
+
relationshipBareFanout(bareName) {
|
|
738
|
+
if (!bareName || typeof bareName !== 'string') return 1;
|
|
739
|
+
this.prebuildRefCountIndex();
|
|
740
|
+
const m = this._refBareRelationshipFanout;
|
|
741
|
+
if (!m || !(m instanceof Map)) return 1;
|
|
742
|
+
return m.get(bareName) || 1;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Batched incoming-call count by entity NAME.
|
|
747
|
+
*
|
|
748
|
+
* For each name in `names`, returns the total number of `relationships`
|
|
749
|
+
* rows of type='calls' whose target is either the bare name or any
|
|
750
|
+
* qualified suffix (e.g. `fastify.decorate`, `app.decorate`). Backed by
|
|
751
|
+
* the lazy in-memory index built by `prebuildRefCountIndex` — first call
|
|
752
|
+
* pays ~10 ms; subsequent calls are sub-millisecond.
|
|
753
|
+
*
|
|
754
|
+
* Counts EXCLUDE 'imports', 'uses', 'extends', 'implements' — only direct
|
|
755
|
+
* `calls` participate. This keeps the signal behavioural (function-was-
|
|
756
|
+
* invoked) rather than structural (type-was-mentioned), matching the
|
|
757
|
+
* Aider PageRank / Cody ref-rank intent.
|
|
758
|
+
*
|
|
759
|
+
* Returns a Map<name, count> with one entry per input name (counts of 0
|
|
760
|
+
* are present, not omitted).
|
|
761
|
+
*
|
|
762
|
+
* @param {string[]} names
|
|
763
|
+
* @returns {Map<string, number>}
|
|
764
|
+
*/
|
|
765
|
+
countIncomingCallsByNames(names) {
|
|
766
|
+
const out = new Map();
|
|
767
|
+
if (!Array.isArray(names) || names.length === 0) return out;
|
|
768
|
+
const uniq = [...new Set(names.filter(n => typeof n === 'string' && n.length >= 2))];
|
|
769
|
+
if (uniq.length === 0) return out;
|
|
770
|
+
this.prebuildRefCountIndex();
|
|
771
|
+
const exact = this._refCountExact || new Map();
|
|
772
|
+
const suffix = this._refSuffixSafe || new Map();
|
|
773
|
+
for (const name of uniq) {
|
|
774
|
+
const dot = name.lastIndexOf('.');
|
|
775
|
+
let c;
|
|
776
|
+
if (dot > 0) {
|
|
777
|
+
c = exact.get(name) || 0;
|
|
778
|
+
} else {
|
|
779
|
+
const agg = suffix.get(name);
|
|
780
|
+
c = agg !== undefined ? agg : (exact.get(name) || 0);
|
|
781
|
+
}
|
|
782
|
+
out.set(name, c);
|
|
783
|
+
}
|
|
784
|
+
return out;
|
|
785
|
+
}
|
|
786
|
+
|
|
70
787
|
/**
|
|
71
788
|
* Get the index timestamp for a file (from stale_since or fallback to mtime).
|
|
72
789
|
*
|
|
@@ -80,7 +797,39 @@ export class CodeGraphRepository {
|
|
|
80
797
|
const db = this._open();
|
|
81
798
|
if (!db) return null;
|
|
82
799
|
try {
|
|
83
|
-
|
|
800
|
+
if (this._hasColumns(db, 'entities', ['epoch_written', 'epoch_retired'])) {
|
|
801
|
+
const visible = db.prepare(`
|
|
802
|
+
SELECT 1 FROM entities
|
|
803
|
+
WHERE file_path = ? AND ${this._entityVisibilitySql(db)}
|
|
804
|
+
LIMIT 1
|
|
805
|
+
`).get(filePath, ...this._entityVisibilityParams(db));
|
|
806
|
+
if (visible) return { staleSince: null };
|
|
807
|
+
|
|
808
|
+
const staleSql = this._manifestEpoch !== null
|
|
809
|
+
? `
|
|
810
|
+
SELECT stale_since
|
|
811
|
+
FROM entities
|
|
812
|
+
WHERE file_path = ?
|
|
813
|
+
AND stale_since IS NOT NULL
|
|
814
|
+
AND (epoch_written IS NULL OR epoch_written <= ?)
|
|
815
|
+
AND (epoch_retired IS NULL OR epoch_retired <= ?)
|
|
816
|
+
ORDER BY stale_since DESC
|
|
817
|
+
LIMIT 1
|
|
818
|
+
`
|
|
819
|
+
: `
|
|
820
|
+
SELECT stale_since
|
|
821
|
+
FROM entities
|
|
822
|
+
WHERE file_path = ?
|
|
823
|
+
AND stale_since IS NOT NULL
|
|
824
|
+
ORDER BY stale_since DESC
|
|
825
|
+
LIMIT 1
|
|
826
|
+
`;
|
|
827
|
+
const staleArgs = this._manifestEpoch !== null
|
|
828
|
+
? [filePath, this._manifestEpoch, this._manifestEpoch]
|
|
829
|
+
: [filePath];
|
|
830
|
+
const staleRow = db.prepare(staleSql).get(...staleArgs);
|
|
831
|
+
return staleRow ? { staleSince: staleRow.stale_since || null } : null;
|
|
832
|
+
}
|
|
84
833
|
const row = db.prepare(`
|
|
85
834
|
SELECT stale_since, MIN(ROWID) as first_row
|
|
86
835
|
FROM entities
|
|
@@ -116,5 +865,8 @@ export class CodeGraphRepository {
|
|
|
116
865
|
this._db.close();
|
|
117
866
|
this._db = null;
|
|
118
867
|
}
|
|
868
|
+
this._hasEntityEpochVisibility = null;
|
|
869
|
+
this._hasRelationshipEpochVisibility = null;
|
|
870
|
+
this._resetDerivedCaches();
|
|
119
871
|
}
|
|
120
872
|
}
|