sweet-search 2.5.2 → 2.5.3
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
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
// Read-only persistence adapter for the unified structural trace surface.
|
|
2
|
+
import Database from 'better-sqlite3';
|
|
3
|
+
import { existsSync, readFileSync } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { applyReadPragmas } from './db-utils.js';
|
|
6
|
+
import { findAliasCallers } from './structural-alias-resolver.js';
|
|
7
|
+
import { rankStructuralCandidates } from './structural-candidate-ranker.js';
|
|
8
|
+
import { findAssignedMemberDefinitions, findSameFileDefinition } from './structural-source-definitions.js';
|
|
9
|
+
import { shouldTrustQualifiedResolution } from './structural-qualified-resolution.js';
|
|
10
|
+
import { fetchPageRank, fetchFrontierBackwardEdges, fetchFrontierForwardEdges } from './structural-graph-signals.js';
|
|
11
|
+
import { CodeGraphReaderVisibility } from './code-graph-visibility.js';
|
|
12
|
+
import { callTargetAliases, clampLimit, isTestPath, lowerCamel, placeholders, qualifiedTargetName, rowToEntity } from './structural-context-utils.js';
|
|
13
|
+
|
|
14
|
+
export class StructuralContextRepository {
|
|
15
|
+
constructor(dbPath, opts = {}) {
|
|
16
|
+
this._readerVisibility = new CodeGraphReaderVisibility(dbPath, opts);
|
|
17
|
+
this.dbPath = this._readerVisibility.dbPath;
|
|
18
|
+
this.projectRoot = opts.projectRoot || process.env.SWEET_SEARCH_PROJECT_ROOT || process.cwd();
|
|
19
|
+
this.db = null;
|
|
20
|
+
this.fileCache = new Map();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_syncAdjacentManifest() {
|
|
24
|
+
const changed = this._readerVisibility.sync(() => this.close());
|
|
25
|
+
this.dbPath = this._readerVisibility.dbPath;
|
|
26
|
+
return changed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
refreshManifestEpoch() {
|
|
30
|
+
this._syncAdjacentManifest();
|
|
31
|
+
return this._readerVisibility.manifestEpoch;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_open() {
|
|
35
|
+
this._syncAdjacentManifest();
|
|
36
|
+
if (!this.db) {
|
|
37
|
+
if (!existsSync(this.dbPath)) return null;
|
|
38
|
+
this.db = new Database(this.dbPath, { readonly: true });
|
|
39
|
+
applyReadPragmas(this.db, { tempStoreMemory: true });
|
|
40
|
+
}
|
|
41
|
+
return this.db;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
close() {
|
|
45
|
+
if (this.db) {
|
|
46
|
+
this.db.close();
|
|
47
|
+
this.db = null;
|
|
48
|
+
}
|
|
49
|
+
this._readerVisibility.reset();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_entityFromRow(row, prefix = '') {
|
|
53
|
+
const entity = rowToEntity(row, prefix);
|
|
54
|
+
const db = this.db;
|
|
55
|
+
if (entity?.summary && db && !this._readerVisibility.summaryVisible(db, entity.id)) {
|
|
56
|
+
return { ...entity, summary: '' };
|
|
57
|
+
}
|
|
58
|
+
return entity;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_entitySql(db, alias = '') { return this._readerVisibility.entitySql(db, alias); }
|
|
62
|
+
_entityParams(db) { return this._readerVisibility.entityParams(db); }
|
|
63
|
+
_relationshipSql(db, alias = 'r') { return this._readerVisibility.relationshipSql(db, alias); }
|
|
64
|
+
_relationshipParams(db) { return this._readerVisibility.relationshipParams(db); }
|
|
65
|
+
|
|
66
|
+
_resolveUnresolvedTarget(targetName) {
|
|
67
|
+
const db = this._open();
|
|
68
|
+
const names = callTargetAliases(targetName);
|
|
69
|
+
if (!db || names.length === 0) return null;
|
|
70
|
+
const entitySql = this._entitySql(db);
|
|
71
|
+
const entityParams = this._entityParams(db);
|
|
72
|
+
const rows = db.prepare(`
|
|
73
|
+
SELECT id, name, type, file_path, start_line, end_line, signature,
|
|
74
|
+
summary, parent_class, package
|
|
75
|
+
FROM entities
|
|
76
|
+
WHERE ${entitySql}
|
|
77
|
+
AND (${names.map(() => 'name = ?').join(' OR ')})
|
|
78
|
+
ORDER BY
|
|
79
|
+
CASE WHEN file_path LIKE '%/test/%' OR file_path LIKE 'test/%' OR file_path LIKE 'tests/%' THEN 1 ELSE 0 END,
|
|
80
|
+
length(name),
|
|
81
|
+
(end_line - start_line) ASC
|
|
82
|
+
LIMIT 1
|
|
83
|
+
`).all(...entityParams, ...names);
|
|
84
|
+
return this._entityFromRow(rows[0]);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_resolveQualifiedAlternative(targetName, excludeId) {
|
|
88
|
+
const db = this._open();
|
|
89
|
+
const name = qualifiedTargetName(targetName);
|
|
90
|
+
if (!db || !name || !excludeId) return null;
|
|
91
|
+
const entitySql = this._entitySql(db);
|
|
92
|
+
const entityParams = this._entityParams(db);
|
|
93
|
+
const rows = db.prepare(`
|
|
94
|
+
SELECT id, name, type, file_path, start_line, end_line, signature,
|
|
95
|
+
summary, parent_class, package
|
|
96
|
+
FROM entities
|
|
97
|
+
WHERE ${entitySql}
|
|
98
|
+
AND lower(name) = lower(?)
|
|
99
|
+
AND id <> ?
|
|
100
|
+
ORDER BY
|
|
101
|
+
CASE WHEN file_path LIKE '%/test/%' OR file_path LIKE 'test/%' OR file_path LIKE 'tests/%' THEN 1 ELSE 0 END,
|
|
102
|
+
CASE type WHEN 'method' THEN 0 WHEN 'function' THEN 1 ELSE 2 END,
|
|
103
|
+
(end_line - start_line) ASC
|
|
104
|
+
LIMIT 8
|
|
105
|
+
`).all(...entityParams, name, excludeId);
|
|
106
|
+
return rows.map(row => this._entityFromRow(row))
|
|
107
|
+
.sort((a, b) => Number(isTestPath(a.filePath)) - Number(isTestPath(b.filePath)))[0] || null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_findAssignedMemberDefinitions(symbol) {
|
|
111
|
+
const db = this._open();
|
|
112
|
+
if (!db || !symbol) return [];
|
|
113
|
+
const entitySql = this._entitySql(db);
|
|
114
|
+
const rows = db.prepare(`
|
|
115
|
+
SELECT DISTINCT file_path FROM entities
|
|
116
|
+
WHERE ${entitySql} AND file_path IS NOT NULL
|
|
117
|
+
ORDER BY CASE WHEN file_path LIKE '%/test/%' OR file_path LIKE 'test/%' OR file_path LIKE 'tests/%' OR file_path LIKE '%/examples/%' OR file_path LIKE 'examples/%' THEN 1 ELSE 0 END, file_path
|
|
118
|
+
LIMIT 120
|
|
119
|
+
`).all(...this._entityParams(db));
|
|
120
|
+
return findAssignedMemberDefinitions({
|
|
121
|
+
name: symbol,
|
|
122
|
+
files: rows.map(r => r.file_path),
|
|
123
|
+
readFileRange: this.readFileRange.bind(this),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
findEntityCandidates(symbol, opts = {}) {
|
|
128
|
+
const db = this._open();
|
|
129
|
+
const raw = String(symbol || '').trim();
|
|
130
|
+
if (!db || !raw) return [];
|
|
131
|
+
|
|
132
|
+
const limit = clampLimit(opts.limit, 12, 50);
|
|
133
|
+
const suffix = raw.includes('.') ? raw.split('.').filter(Boolean).pop() : raw;
|
|
134
|
+
const names = [...new Set([raw, suffix].filter(Boolean))];
|
|
135
|
+
const filePath = typeof opts.filePath === 'string' && opts.filePath.trim()
|
|
136
|
+
? opts.filePath.trim()
|
|
137
|
+
: null;
|
|
138
|
+
const nameWhere = names.map(() => 'lower(name) = lower(?)').join(' OR ');
|
|
139
|
+
const params = [...names];
|
|
140
|
+
const entitySql = this._entitySql(db);
|
|
141
|
+
const entityParams = this._entityParams(db);
|
|
142
|
+
let fileWhere = '';
|
|
143
|
+
if (filePath) {
|
|
144
|
+
fileWhere = 'AND (file_path = ? OR file_path LIKE ?)';
|
|
145
|
+
params.push(filePath, `%${filePath}%`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const exactRows = db.prepare(`
|
|
149
|
+
SELECT id, name, type, file_path, start_line, end_line, signature,
|
|
150
|
+
summary, parent_class, package
|
|
151
|
+
FROM entities
|
|
152
|
+
WHERE ${entitySql}
|
|
153
|
+
AND (${nameWhere})
|
|
154
|
+
${fileWhere}
|
|
155
|
+
ORDER BY
|
|
156
|
+
CASE
|
|
157
|
+
WHEN name = ? THEN 0
|
|
158
|
+
WHEN lower(name) = lower(?) THEN 1
|
|
159
|
+
ELSE 2
|
|
160
|
+
END,
|
|
161
|
+
CASE WHEN file_path LIKE '%/test/%' OR file_path LIKE 'test/%' OR file_path LIKE 'tests/%' THEN 1 ELSE 0 END,
|
|
162
|
+
CASE type
|
|
163
|
+
WHEN 'class' THEN 0 WHEN 'struct' THEN 0 WHEN 'trait' THEN 0
|
|
164
|
+
WHEN 'interface' THEN 1 WHEN 'enum' THEN 1 WHEN 'type' THEN 1 WHEN 'typeAlias' THEN 1
|
|
165
|
+
WHEN 'function' THEN 2 WHEN 'method' THEN 2
|
|
166
|
+
ELSE 3
|
|
167
|
+
END,
|
|
168
|
+
(end_line - start_line) ASC
|
|
169
|
+
LIMIT ?
|
|
170
|
+
`).all(...entityParams, ...params, raw, raw, limit);
|
|
171
|
+
if (exactRows.length) {
|
|
172
|
+
const members = this._findAssignedMemberDefinitions(raw);
|
|
173
|
+
const candidates = [...members, ...exactRows.map(row => this._entityFromRow(row))].filter(Boolean);
|
|
174
|
+
return rankStructuralCandidates(candidates, { queryHint: opts.queryHint, readFileRange: this.readFileRange.bind(this) });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (raw.length < 3) return [];
|
|
178
|
+
const members = this._findAssignedMemberDefinitions(raw);
|
|
179
|
+
const likeParams = [`%${raw}%`];
|
|
180
|
+
let likeFileWhere = '';
|
|
181
|
+
if (filePath) {
|
|
182
|
+
likeFileWhere = 'AND (file_path = ? OR file_path LIKE ?)';
|
|
183
|
+
likeParams.push(filePath, `%${filePath}%`);
|
|
184
|
+
}
|
|
185
|
+
const likeRows = db.prepare(`
|
|
186
|
+
SELECT id, name, type, file_path, start_line, end_line, signature,
|
|
187
|
+
summary, parent_class, package
|
|
188
|
+
FROM entities
|
|
189
|
+
WHERE ${entitySql}
|
|
190
|
+
AND lower(name) LIKE lower(?)
|
|
191
|
+
${likeFileWhere}
|
|
192
|
+
ORDER BY
|
|
193
|
+
CASE WHEN file_path LIKE '%/test/%' OR file_path LIKE 'test/%' OR file_path LIKE 'tests/%' THEN 1 ELSE 0 END,
|
|
194
|
+
length(name), (end_line - start_line) ASC
|
|
195
|
+
LIMIT ?
|
|
196
|
+
`).all(...entityParams, ...likeParams, limit).map(row => this._entityFromRow(row));
|
|
197
|
+
return rankStructuralCandidates([...members, ...likeRows].filter(Boolean), { queryHint: opts.queryHint, readFileRange: this.readFileRange.bind(this) });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
getCallers(target, opts = {}) {
|
|
201
|
+
const db = this._open();
|
|
202
|
+
if (!db || !target?.id) return [];
|
|
203
|
+
const limit = clampLimit(opts.limit, 120, 500);
|
|
204
|
+
const types = opts.types?.length ? opts.types : ['calls', 'uses', 'implements', 'extends', 'overrides'];
|
|
205
|
+
const patterns = [
|
|
206
|
+
target.name,
|
|
207
|
+
`${target.name}.%`,
|
|
208
|
+
`${lowerCamel(target.name)}.%`,
|
|
209
|
+
`%.${target.name}`,
|
|
210
|
+
`%::${target.name}`,
|
|
211
|
+
];
|
|
212
|
+
const entitySql = this._entitySql(db, 'e');
|
|
213
|
+
const relationshipSql = this._relationshipSql(db, 'r');
|
|
214
|
+
const rows = db.prepare(`
|
|
215
|
+
SELECT DISTINCT
|
|
216
|
+
e.id, e.name, e.type, e.file_path, e.start_line, e.end_line,
|
|
217
|
+
e.signature, e.summary, e.parent_class, e.package,
|
|
218
|
+
r.context_line, r.target_name, r.weight, r.type as rel_type
|
|
219
|
+
FROM relationships r
|
|
220
|
+
JOIN entities e ON e.id = r.source_id
|
|
221
|
+
WHERE r.type IN (${placeholders(types)})
|
|
222
|
+
AND ${entitySql}
|
|
223
|
+
AND ${relationshipSql}
|
|
224
|
+
AND e.id <> ?
|
|
225
|
+
AND (
|
|
226
|
+
r.target_id = ?
|
|
227
|
+
OR r.target_name = ?
|
|
228
|
+
OR r.target_name LIKE ?
|
|
229
|
+
OR r.target_name LIKE ?
|
|
230
|
+
OR r.target_name LIKE ?
|
|
231
|
+
OR r.target_name LIKE ?
|
|
232
|
+
)
|
|
233
|
+
ORDER BY r.weight DESC, e.file_path, r.context_line
|
|
234
|
+
LIMIT ?
|
|
235
|
+
`).all(...types, ...this._entityParams(db), ...this._relationshipParams(db), target.id, target.id, ...patterns, limit);
|
|
236
|
+
return rows.map(row => ({
|
|
237
|
+
...this._entityFromRow(row),
|
|
238
|
+
relationship: row.rel_type,
|
|
239
|
+
contextLine: row.context_line || null,
|
|
240
|
+
targetName: row.target_name || null,
|
|
241
|
+
weight: row.weight ?? 1,
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getAliasCallers(target, opts = {}) {
|
|
246
|
+
const db = this._open();
|
|
247
|
+
if (!db || !target?.id) return [];
|
|
248
|
+
return findAliasCallers({ db, target, readFileRange: this.readFileRange.bind(this), limit: clampLimit(opts.limit, 40, 200), entityVisibilitySql: this._entitySql(db), entityVisibilityParams: this._entityParams(db), mapEntity: row => this._entityFromRow(row) });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
getCallees(target, opts = {}) {
|
|
252
|
+
const db = this._open();
|
|
253
|
+
if (!db || !target?.id) return [];
|
|
254
|
+
const limit = clampLimit(opts.limit, 120, 500);
|
|
255
|
+
const entitySql = this._entitySql(db, 'e');
|
|
256
|
+
const relationshipSql = this._relationshipSql(db, 'r');
|
|
257
|
+
const rows = db.prepare(`
|
|
258
|
+
SELECT
|
|
259
|
+
e.id, e.name, e.type, e.file_path, e.start_line, e.end_line,
|
|
260
|
+
e.signature, e.summary, e.parent_class, e.package,
|
|
261
|
+
r.context_line, r.target_name, r.weight, r.type as rel_type
|
|
262
|
+
FROM relationships r
|
|
263
|
+
LEFT JOIN entities e ON e.id = r.target_id AND ${entitySql}
|
|
264
|
+
WHERE r.source_id = ?
|
|
265
|
+
AND r.type = 'calls'
|
|
266
|
+
AND ${relationshipSql}
|
|
267
|
+
ORDER BY r.context_line, r.weight DESC
|
|
268
|
+
LIMIT ?
|
|
269
|
+
`).all(...this._entityParams(db), target.id, ...this._relationshipParams(db), limit);
|
|
270
|
+
return rows.map((row, idx) => {
|
|
271
|
+
let resolved = row.id ? this._entityFromRow(row) : (this._resolveUnresolvedTarget(row.target_name) || {
|
|
272
|
+
id: `external:${idx}:${row.target_name || 'unknown'}`,
|
|
273
|
+
name: row.target_name || 'external',
|
|
274
|
+
type: 'external',
|
|
275
|
+
filePath: null,
|
|
276
|
+
startLine: null,
|
|
277
|
+
endLine: null,
|
|
278
|
+
signature: row.target_name || '',
|
|
279
|
+
summary: '',
|
|
280
|
+
});
|
|
281
|
+
if (row.id && !shouldTrustQualifiedResolution(row.target_name, resolved)) resolved = { id: `external:${idx}:${row.target_name || 'unknown'}`, name: row.target_name || 'external', type: 'external', filePath: null, startLine: null, endLine: null, signature: row.target_name || '', summary: '' };
|
|
282
|
+
if (resolved.id === target.id) {
|
|
283
|
+
resolved = this._resolveQualifiedAlternative(row.target_name, target.id) || resolved;
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
...resolved,
|
|
287
|
+
relationship: row.rel_type,
|
|
288
|
+
contextLine: row.context_line || null,
|
|
289
|
+
targetName: row.target_name || null,
|
|
290
|
+
weight: row.weight ?? 1,
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
getReverseDependents(frontierIds, target, opts = {}) {
|
|
296
|
+
const db = this._open();
|
|
297
|
+
const ids = [...new Set((frontierIds || []).filter(Boolean))];
|
|
298
|
+
if (!db || ids.length === 0) return [];
|
|
299
|
+
const limit = clampLimit(opts.limit, 160, 1000);
|
|
300
|
+
const types = opts.types?.length
|
|
301
|
+
? opts.types
|
|
302
|
+
: ['calls', 'uses', 'implements', 'extends', 'overrides'];
|
|
303
|
+
const includeNamePattern = opts.includeNamePattern === true && target?.name;
|
|
304
|
+
|
|
305
|
+
const nameClause = includeNamePattern
|
|
306
|
+
? `OR r.target_name = ? OR r.target_name LIKE ? OR r.target_name LIKE ? OR r.target_name LIKE ?`
|
|
307
|
+
: '';
|
|
308
|
+
const nameParams = includeNamePattern
|
|
309
|
+
? [target.name, `${lowerCamel(target.name)}.%`, `%.${target.name}`, `%::${target.name}`]
|
|
310
|
+
: [];
|
|
311
|
+
const entitySql = this._entitySql(db, 'e');
|
|
312
|
+
const relationshipSql = this._relationshipSql(db, 'r');
|
|
313
|
+
|
|
314
|
+
const rows = db.prepare(`
|
|
315
|
+
SELECT DISTINCT
|
|
316
|
+
e.id, e.name, e.type, e.file_path, e.start_line, e.end_line,
|
|
317
|
+
e.signature, e.summary, e.parent_class, e.package,
|
|
318
|
+
r.target_id, r.target_name, r.context_line, r.weight, r.type as rel_type
|
|
319
|
+
FROM relationships r
|
|
320
|
+
JOIN entities e ON e.id = r.source_id
|
|
321
|
+
WHERE ${entitySql}
|
|
322
|
+
AND ${relationshipSql}
|
|
323
|
+
AND r.type IN (${placeholders(types)})
|
|
324
|
+
AND (r.target_id IN (${placeholders(ids)}) ${nameClause})
|
|
325
|
+
ORDER BY r.weight DESC, e.file_path, r.context_line
|
|
326
|
+
LIMIT ?
|
|
327
|
+
`).all(...this._entityParams(db), ...this._relationshipParams(db), ...types, ...ids, ...nameParams, limit);
|
|
328
|
+
|
|
329
|
+
return rows.map(row => ({
|
|
330
|
+
...this._entityFromRow(row),
|
|
331
|
+
relationship: row.rel_type,
|
|
332
|
+
targetId: row.target_id || null,
|
|
333
|
+
targetName: row.target_name || null,
|
|
334
|
+
contextLine: row.context_line || null,
|
|
335
|
+
weight: row.weight ?? 1,
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
getForwardDependencies(frontierIds, opts = {}) {
|
|
340
|
+
const db = this._open();
|
|
341
|
+
const ids = [...new Set((frontierIds || []).filter(id => id && !String(id).startsWith('external:')))];
|
|
342
|
+
if (!db || ids.length === 0) return [];
|
|
343
|
+
const limit = clampLimit(opts.limit, 160, 1000);
|
|
344
|
+
const types = opts.types?.length
|
|
345
|
+
? opts.types
|
|
346
|
+
: ['calls', 'uses', 'implements', 'extends', 'overrides'];
|
|
347
|
+
const entitySql = this._entitySql(db, 'e');
|
|
348
|
+
const relationshipSql = this._relationshipSql(db, 'r');
|
|
349
|
+
|
|
350
|
+
const rows = db.prepare(`
|
|
351
|
+
SELECT
|
|
352
|
+
r.source_id, r.target_id, r.target_name, r.context_line, r.weight, r.type as rel_type,
|
|
353
|
+
e.id, e.name, e.type, e.file_path, e.start_line, e.end_line,
|
|
354
|
+
e.signature, e.summary, e.parent_class, e.package
|
|
355
|
+
FROM relationships r
|
|
356
|
+
LEFT JOIN entities e ON e.id = r.target_id AND ${entitySql}
|
|
357
|
+
WHERE r.source_id IN (${placeholders(ids)})
|
|
358
|
+
AND r.type IN (${placeholders(types)})
|
|
359
|
+
AND ${relationshipSql}
|
|
360
|
+
ORDER BY r.weight DESC, r.source_id, r.context_line
|
|
361
|
+
LIMIT ?
|
|
362
|
+
`).all(...this._entityParams(db), ...ids, ...types, ...this._relationshipParams(db), limit);
|
|
363
|
+
|
|
364
|
+
return rows.map((row, idx) => {
|
|
365
|
+
let resolved = row.id ? this._entityFromRow(row) : (this._resolveUnresolvedTarget(row.target_name) || {
|
|
366
|
+
id: `external:${row.source_id}:${idx}:${row.target_name || 'unknown'}`,
|
|
367
|
+
name: row.target_name || 'external',
|
|
368
|
+
type: 'external',
|
|
369
|
+
filePath: null,
|
|
370
|
+
startLine: null,
|
|
371
|
+
endLine: null,
|
|
372
|
+
signature: row.target_name || '',
|
|
373
|
+
summary: '',
|
|
374
|
+
});
|
|
375
|
+
if (row.id && !shouldTrustQualifiedResolution(row.target_name, resolved)) resolved = { id: `external:${row.source_id}:${idx}:${row.target_name || 'unknown'}`, name: row.target_name || 'external', type: 'external', filePath: null, startLine: null, endLine: null, signature: row.target_name || '', summary: '' };
|
|
376
|
+
if (resolved.id === row.source_id) {
|
|
377
|
+
resolved = this._resolveQualifiedAlternative(row.target_name, row.source_id) || resolved;
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
...resolved,
|
|
381
|
+
relationship: row.rel_type,
|
|
382
|
+
sourceId: row.source_id,
|
|
383
|
+
targetId: row.target_id || null,
|
|
384
|
+
targetName: row.target_name || null,
|
|
385
|
+
contextLine: row.context_line || null,
|
|
386
|
+
weight: row.weight ?? 1,
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
getFanCounts(entityIds) {
|
|
392
|
+
const db = this._open();
|
|
393
|
+
const ids = [...new Set((entityIds || []).filter(id => id && !String(id).startsWith('external:')))];
|
|
394
|
+
const out = new Map(ids.map(id => [id, { fanIn: 0, fanOut: 0 }]));
|
|
395
|
+
if (!db || ids.length === 0) return out;
|
|
396
|
+
const relationshipSql = this._relationshipSql(db, '');
|
|
397
|
+
const relationshipParams = this._relationshipParams(db);
|
|
398
|
+
|
|
399
|
+
const inRows = db.prepare(`
|
|
400
|
+
SELECT target_id as id, COUNT(DISTINCT source_id) as n
|
|
401
|
+
FROM relationships
|
|
402
|
+
WHERE target_id IN (${placeholders(ids)})
|
|
403
|
+
AND ${relationshipSql}
|
|
404
|
+
GROUP BY target_id
|
|
405
|
+
`).all(...ids, ...relationshipParams);
|
|
406
|
+
for (const row of inRows) {
|
|
407
|
+
if (out.has(row.id)) out.get(row.id).fanIn = row.n || 0;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const outRows = db.prepare(`
|
|
411
|
+
SELECT source_id as id, COUNT(DISTINCT COALESCE(target_id, target_name)) as n
|
|
412
|
+
FROM relationships
|
|
413
|
+
WHERE source_id IN (${placeholders(ids)})
|
|
414
|
+
AND ${relationshipSql}
|
|
415
|
+
GROUP BY source_id
|
|
416
|
+
`).all(...ids, ...relationshipParams);
|
|
417
|
+
for (const row of outRows) {
|
|
418
|
+
if (out.has(row.id)) out.get(row.id).fanOut = row.n || 0;
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Precomputed PageRank values for a batch of entity IDs (0 for missing). */
|
|
424
|
+
getPageRank(entityIds) {
|
|
425
|
+
const db = this._open();
|
|
426
|
+
return fetchPageRank(db, entityIds, { entityVisibilitySql: db ? this._entitySql(db) : undefined, entityVisibilityParams: db ? this._entityParams(db) : undefined });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** One-hop reverse edges (callers) for Forward Push backward subgraph. */
|
|
430
|
+
getFrontierBackwardEdges(frontierIds, opts = {}) {
|
|
431
|
+
const db = this._open();
|
|
432
|
+
return fetchFrontierBackwardEdges(db, frontierIds, { ...opts, relationshipVisibilitySql: db ? this._relationshipSql(db, '') : undefined, relationshipVisibilityParams: db ? this._relationshipParams(db) : undefined });
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** One-hop forward edges (callees) for Forward Push forward subgraph. */
|
|
436
|
+
getFrontierForwardEdges(frontierIds, opts = {}) {
|
|
437
|
+
const db = this._open();
|
|
438
|
+
return fetchFrontierForwardEdges(db, frontierIds, { ...opts, relationshipVisibilitySql: db ? this._relationshipSql(db, '') : undefined, relationshipVisibilityParams: db ? this._relationshipParams(db) : undefined });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
findSameFileDefinition(name, filePath) { return findSameFileDefinition({ name, filePath, readFileRange: this.readFileRange.bind(this) }); }
|
|
442
|
+
|
|
443
|
+
getEntityCount() {
|
|
444
|
+
const db = this._open();
|
|
445
|
+
if (!db) return 0;
|
|
446
|
+
return db.prepare(`SELECT COUNT(*) as n FROM entities WHERE ${this._entitySql(db)}`)
|
|
447
|
+
.get(...this._entityParams(db))?.n || 0;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
readFileRange(filePath, startLine, endLine) {
|
|
451
|
+
if (!filePath) return null;
|
|
452
|
+
try {
|
|
453
|
+
const root = this.projectRoot;
|
|
454
|
+
const abs = path.isAbsolute(filePath) ? filePath : path.join(root, filePath);
|
|
455
|
+
const resolved = path.resolve(abs);
|
|
456
|
+
const resolvedRoot = path.resolve(root);
|
|
457
|
+
if (!resolved.startsWith(resolvedRoot + path.sep) && resolved !== resolvedRoot) return null;
|
|
458
|
+
let lines = this.fileCache.get(resolved);
|
|
459
|
+
if (!lines) {
|
|
460
|
+
lines = readFileSync(resolved, 'utf8').split('\n');
|
|
461
|
+
this.fileCache.set(resolved, lines);
|
|
462
|
+
}
|
|
463
|
+
const start = Math.max(1, Number.parseInt(startLine || 1, 10));
|
|
464
|
+
const end = Math.max(start, Number.parseInt(endLine || start, 10));
|
|
465
|
+
return lines.slice(start - 1, end).join('\n');
|
|
466
|
+
} catch {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export default StructuralContextRepository;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function clampLimit(value, fallback, max) {
|
|
2
|
+
const n = Number.parseInt(value ?? fallback, 10);
|
|
3
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
4
|
+
return Math.max(1, Math.min(max, n));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function lowerCamel(name) {
|
|
8
|
+
if (!name) return '';
|
|
9
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function rowToEntity(row, prefix = '') {
|
|
13
|
+
if (!row) return null;
|
|
14
|
+
return {
|
|
15
|
+
id: row[`${prefix}id`],
|
|
16
|
+
name: row[`${prefix}name`],
|
|
17
|
+
type: row[`${prefix}type`],
|
|
18
|
+
filePath: row[`${prefix}file_path`],
|
|
19
|
+
startLine: row[`${prefix}start_line`],
|
|
20
|
+
endLine: row[`${prefix}end_line`],
|
|
21
|
+
signature: row[`${prefix}signature`] || '',
|
|
22
|
+
summary: row[`${prefix}summary`] || '',
|
|
23
|
+
parentClass: row[`${prefix}parent_class`] || null,
|
|
24
|
+
package: row[`${prefix}package`] || null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function callTargetAliases(targetName) {
|
|
29
|
+
const raw = String(targetName || '').trim();
|
|
30
|
+
if (!raw) return [];
|
|
31
|
+
const out = [raw];
|
|
32
|
+
const bound = raw.match(/^(.+)\.(bind|call|apply)$/);
|
|
33
|
+
if (bound) out.push(bound[1]);
|
|
34
|
+
if (raw.includes('::')) out.push(raw.split('::').filter(Boolean).pop());
|
|
35
|
+
return [...new Set(out)];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function qualifiedTargetName(targetName) {
|
|
39
|
+
const base = String(targetName || '').trim().replace(/\.(bind|call|apply)$/, '').replace(/::/g, '.');
|
|
40
|
+
if (!base.includes('.')) return null;
|
|
41
|
+
const parts = base.split('.').filter(Boolean);
|
|
42
|
+
return parts.length > 1 ? parts[parts.length - 1] : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isTestPath(filePath = '') {
|
|
46
|
+
return /(^|\/)(__tests__|tests?|spec|fixtures|examples?|docs?)(\/|$)|[-_.](test|spec)\.[cm]?[jt]sx?$|_test\.go$/.test(filePath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function placeholders(values) {
|
|
50
|
+
return values.map(() => '?').join(',');
|
|
51
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence helpers for structural graph importance signals.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from structural-context-repository.js so the repository stays
|
|
5
|
+
* under the 500-line ceiling. These functions all consume an open
|
|
6
|
+
* better-sqlite3 handle plus a list of entity IDs and return primitive
|
|
7
|
+
* values; they hold no state and live in infrastructure/ alongside the
|
|
8
|
+
* repository they support.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
function placeholders(values) {
|
|
12
|
+
return values.map(() => '?').join(',');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function dropExternalIds(ids) {
|
|
16
|
+
return [...new Set((ids || []).filter(id => id && !String(id).startsWith('external:')))];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function clampLimit(value, fallback, max) {
|
|
20
|
+
const n = Number.parseInt(value ?? fallback, 10);
|
|
21
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
22
|
+
return Math.max(1, Math.min(max, n));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEFAULT_REL_TYPES = ['calls', 'uses', 'implements', 'extends', 'overrides'];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Batch-fetch precomputed PageRank values from the entities table.
|
|
29
|
+
*
|
|
30
|
+
* Returns 0 for any ID that's missing, external, or whose row predates the
|
|
31
|
+
* `page_rank` column. Index-build populates this column via
|
|
32
|
+
* core/graph/structural-pagerank.js#populatePageRankColumn.
|
|
33
|
+
*
|
|
34
|
+
* @param {import('better-sqlite3').Database} db
|
|
35
|
+
* @param {Array<string|number>} entityIds
|
|
36
|
+
* @returns {Map<string|number, number>}
|
|
37
|
+
*/
|
|
38
|
+
export function fetchPageRank(db, entityIds, opts = {}) {
|
|
39
|
+
const ids = dropExternalIds(entityIds);
|
|
40
|
+
const out = new Map(ids.map(id => [id, 0]));
|
|
41
|
+
if (!db || ids.length === 0) return out;
|
|
42
|
+
const entitySql = opts.entityVisibilitySql || 'stale_since IS NULL';
|
|
43
|
+
const entityParams = opts.entityVisibilityParams || [];
|
|
44
|
+
try {
|
|
45
|
+
const rows = db.prepare(`
|
|
46
|
+
SELECT id, COALESCE(page_rank, 0) AS pr
|
|
47
|
+
FROM entities
|
|
48
|
+
WHERE id IN (${placeholders(ids)}) AND ${entitySql}
|
|
49
|
+
`).all(...ids, ...entityParams);
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
if (out.has(row.id)) out.set(row.id, Number.isFinite(row.pr) ? row.pr : 0);
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// Legacy DBs (or test fixtures) lack page_rank — leave zeros.
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load incoming one-hop edges for a frontier of entity IDs ("who calls these").
|
|
61
|
+
*
|
|
62
|
+
* Powers the backward Forward-Push subgraph used to rank callers by their
|
|
63
|
+
* importance relative to the target symbol.
|
|
64
|
+
*
|
|
65
|
+
* @param {import('better-sqlite3').Database} db
|
|
66
|
+
* @param {Array<string|number>} frontierIds
|
|
67
|
+
* @param {object} [opts]
|
|
68
|
+
* @param {string[]} [opts.types]
|
|
69
|
+
* @param {number} [opts.limit=4000]
|
|
70
|
+
* @returns {Array<{ from: string|number, to: string|number, weight: number }>}
|
|
71
|
+
*/
|
|
72
|
+
export function fetchFrontierBackwardEdges(db, frontierIds, opts = {}) {
|
|
73
|
+
const ids = dropExternalIds(frontierIds);
|
|
74
|
+
if (!db || ids.length === 0) return [];
|
|
75
|
+
const limit = clampLimit(opts.limit, 4000, 20000);
|
|
76
|
+
const types = opts.types?.length ? opts.types : DEFAULT_REL_TYPES;
|
|
77
|
+
const relationshipSql = opts.relationshipVisibilitySql || '1=1';
|
|
78
|
+
const relationshipParams = opts.relationshipVisibilityParams || [];
|
|
79
|
+
const rows = db.prepare(`
|
|
80
|
+
SELECT source_id, target_id, COALESCE(weight, 1.0) AS weight
|
|
81
|
+
FROM relationships
|
|
82
|
+
WHERE target_id IN (${placeholders(ids)})
|
|
83
|
+
AND source_id IS NOT NULL
|
|
84
|
+
AND type IN (${placeholders(types)})
|
|
85
|
+
AND ${relationshipSql}
|
|
86
|
+
LIMIT ?
|
|
87
|
+
`).all(...ids, ...types, ...relationshipParams, limit);
|
|
88
|
+
return rows.map(row => ({ from: row.target_id, to: row.source_id, weight: row.weight }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Load outgoing one-hop edges for a frontier of entity IDs ("what these call").
|
|
93
|
+
*
|
|
94
|
+
* Powers the forward Forward-Push subgraph used to rank callees by their
|
|
95
|
+
* relevance to the target symbol's downstream flow.
|
|
96
|
+
*
|
|
97
|
+
* @param {import('better-sqlite3').Database} db
|
|
98
|
+
* @param {Array<string|number>} frontierIds
|
|
99
|
+
* @param {object} [opts]
|
|
100
|
+
* @param {string[]} [opts.types]
|
|
101
|
+
* @param {number} [opts.limit=4000]
|
|
102
|
+
* @returns {Array<{ from: string|number, to: string|number, weight: number }>}
|
|
103
|
+
*/
|
|
104
|
+
export function fetchFrontierForwardEdges(db, frontierIds, opts = {}) {
|
|
105
|
+
const ids = dropExternalIds(frontierIds);
|
|
106
|
+
if (!db || ids.length === 0) return [];
|
|
107
|
+
const limit = clampLimit(opts.limit, 4000, 20000);
|
|
108
|
+
const types = opts.types?.length ? opts.types : DEFAULT_REL_TYPES;
|
|
109
|
+
const relationshipSql = opts.relationshipVisibilitySql || '1=1';
|
|
110
|
+
const relationshipParams = opts.relationshipVisibilityParams || [];
|
|
111
|
+
const rows = db.prepare(`
|
|
112
|
+
SELECT source_id, target_id, COALESCE(weight, 1.0) AS weight
|
|
113
|
+
FROM relationships
|
|
114
|
+
WHERE source_id IN (${placeholders(ids)})
|
|
115
|
+
AND target_id IS NOT NULL
|
|
116
|
+
AND type IN (${placeholders(types)})
|
|
117
|
+
AND ${relationshipSql}
|
|
118
|
+
LIMIT ?
|
|
119
|
+
`).all(...ids, ...types, ...relationshipParams, limit);
|
|
120
|
+
return rows.map(row => ({ from: row.source_id, to: row.target_id, weight: row.weight }));
|
|
121
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
function qualifierTerms(qualifier) {
|
|
2
|
+
const raw = String(qualifier || '').toLowerCase();
|
|
3
|
+
return [raw, ...raw.split(/[^a-z0-9]+/)].filter(t => t.length >= 3);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function shouldTrustQualifiedResolution(targetName, entity) {
|
|
7
|
+
const normalized = String(targetName || '').replace(/::/g, '.');
|
|
8
|
+
const parts = normalized.split('.').filter(Boolean);
|
|
9
|
+
if (parts.length < 2 || !entity?.name) return true;
|
|
10
|
+
const leaf = parts[parts.length - 1].toLowerCase();
|
|
11
|
+
if (leaf !== String(entity.name).toLowerCase()) return true;
|
|
12
|
+
const qualifier = parts[parts.length - 2];
|
|
13
|
+
const hay = `${entity.filePath || ''} ${entity.parentClass || ''} ${entity.package || ''} ${entity.signature || ''} ${entity.summary || ''}`.toLowerCase();
|
|
14
|
+
return qualifierTerms(qualifier).some(term => hay.includes(term));
|
|
15
|
+
}
|