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.
Files changed (155) hide show
  1. package/core/cli.js +24 -3
  2. package/core/graph/graph-expansion.js +215 -36
  3. package/core/graph/graph-extractor.js +196 -11
  4. package/core/graph/graph-search.js +395 -92
  5. package/core/graph/hcgs-generator.js +2 -1
  6. package/core/graph/index.js +2 -0
  7. package/core/graph/repo-map.js +28 -6
  8. package/core/graph/structural-answer-cues.js +168 -0
  9. package/core/graph/structural-callsite-hints.js +40 -0
  10. package/core/graph/structural-context-format.js +40 -0
  11. package/core/graph/structural-context.js +450 -0
  12. package/core/graph/structural-forward-push.js +156 -0
  13. package/core/graph/structural-header-context.js +19 -0
  14. package/core/graph/structural-importance.js +148 -0
  15. package/core/graph/structural-pagerank.js +197 -0
  16. package/core/graph/summary-manager.js +13 -9
  17. package/core/incremental-indexing/application/dirty-scan.mjs +236 -0
  18. package/core/incremental-indexing/application/file-watcher.mjs +197 -0
  19. package/core/incremental-indexing/application/maintenance-handlers.mjs +519 -0
  20. package/core/incremental-indexing/application/maintenance-worker.mjs +380 -0
  21. package/core/incremental-indexing/application/operator-cli.mjs +554 -0
  22. package/core/incremental-indexing/application/production-li-delta.mjs +192 -0
  23. package/core/incremental-indexing/application/production-reconciler-helpers.mjs +107 -0
  24. package/core/incremental-indexing/application/production-reconciler.mjs +583 -0
  25. package/core/incremental-indexing/application/reconciler.mjs +477 -0
  26. package/core/incremental-indexing/application/tombstone-injector.mjs +148 -0
  27. package/core/incremental-indexing/domain/chunk-identity.mjs +260 -0
  28. package/core/incremental-indexing/domain/encoder-deps.mjs +193 -0
  29. package/core/incremental-indexing/domain/encoder-input.mjs +225 -0
  30. package/core/incremental-indexing/domain/interval-autotune.mjs +255 -0
  31. package/core/incremental-indexing/domain/reconcile-counters.mjs +149 -0
  32. package/core/incremental-indexing/domain/watermark-scheduler.mjs +239 -0
  33. package/core/incremental-indexing/infrastructure/artifact-temp-sweep.mjs +163 -0
  34. package/core/incremental-indexing/infrastructure/baseline-readiness.mjs +121 -0
  35. package/core/incremental-indexing/infrastructure/dirty-set.mjs +233 -0
  36. package/core/incremental-indexing/infrastructure/graph-gc.mjs +314 -0
  37. package/core/incremental-indexing/infrastructure/hashing.mjs +298 -0
  38. package/core/incremental-indexing/infrastructure/hcgs-invalidation.mjs +182 -0
  39. package/core/incremental-indexing/infrastructure/li-segment-merge.mjs +278 -0
  40. package/core/incremental-indexing/infrastructure/li-segment-state.mjs +173 -0
  41. package/core/incremental-indexing/infrastructure/lockfile.mjs +119 -0
  42. package/core/incremental-indexing/infrastructure/maintenance-state-reader.mjs +283 -0
  43. package/core/incremental-indexing/infrastructure/manifest.mjs +194 -0
  44. package/core/incremental-indexing/infrastructure/path-filter.mjs +190 -0
  45. package/core/incremental-indexing/infrastructure/reader-heartbeat.mjs +201 -0
  46. package/core/incremental-indexing/infrastructure/schema-migrations.mjs +257 -0
  47. package/core/incremental-indexing/infrastructure/sparse-gram-delta.mjs +335 -0
  48. package/core/incremental-indexing/infrastructure/sqlite-fts5.mjs +176 -0
  49. package/core/incremental-indexing/infrastructure/staleness-display.mjs +105 -0
  50. package/core/incremental-indexing/infrastructure/tombstone-bitmap.mjs +234 -0
  51. package/core/incremental-indexing/infrastructure/vector-delta-writer.mjs +359 -0
  52. package/core/incremental-indexing/infrastructure/vector-gc.mjs +133 -0
  53. package/core/incremental-indexing/infrastructure/worktree-stamp.mjs +155 -0
  54. package/core/incremental-indexing/infrastructure/wsl2-detect.mjs +115 -0
  55. package/core/indexing/admission-policy.js +139 -0
  56. package/core/indexing/artifact-builder.js +29 -12
  57. package/core/indexing/ast-chunker.js +107 -30
  58. package/core/indexing/dedup/exemplar-selector.js +19 -1
  59. package/core/indexing/gitignore-filter.js +223 -0
  60. package/core/indexing/incremental-tracker.js +99 -30
  61. package/core/indexing/index-codebase-v21.js +6 -5
  62. package/core/indexing/index-maintainer.mjs +698 -6
  63. package/core/indexing/indexer-ann.js +99 -15
  64. package/core/indexing/indexer-build.js +158 -45
  65. package/core/indexing/indexer-empty-baseline.js +80 -0
  66. package/core/indexing/indexer-manifest.js +66 -0
  67. package/core/indexing/indexer-phases.js +56 -23
  68. package/core/indexing/indexer-sparse-gram.js +54 -13
  69. package/core/indexing/indexer-utils.js +26 -208
  70. package/core/indexing/indexing-file-policy.js +32 -7
  71. package/core/indexing/maintainer-launcher.mjs +137 -0
  72. package/core/indexing/merkle-tracker.js +251 -244
  73. package/core/indexing/model-pool.js +46 -5
  74. package/core/infrastructure/code-graph-repository.js +758 -6
  75. package/core/infrastructure/code-graph-visibility.js +157 -0
  76. package/core/infrastructure/codebase-repository.js +100 -13
  77. package/core/infrastructure/config/search.js +1 -1
  78. package/core/infrastructure/db-utils.js +118 -0
  79. package/core/infrastructure/dedup-hashing.js +10 -13
  80. package/core/infrastructure/hardware-capability.js +17 -7
  81. package/core/infrastructure/index.js +8 -2
  82. package/core/infrastructure/language-patterns/maps.js +4 -1
  83. package/core/infrastructure/language-patterns/registry-core.js +56 -17
  84. package/core/infrastructure/language-patterns/registry-object-oriented.js +12 -5
  85. package/core/infrastructure/language-patterns.js +69 -0
  86. package/core/infrastructure/model-registry.js +20 -0
  87. package/core/infrastructure/native-inference.js +7 -12
  88. package/core/infrastructure/native-resolver.js +52 -37
  89. package/core/infrastructure/native-sparse-gram.js +261 -20
  90. package/core/infrastructure/native-tokenizer.js +6 -15
  91. package/core/infrastructure/simd-distance.js +10 -16
  92. package/core/infrastructure/sparse-gram-delta-reader.js +76 -0
  93. package/core/infrastructure/structural-alias-resolver.js +122 -0
  94. package/core/infrastructure/structural-candidate-ranker.js +34 -0
  95. package/core/infrastructure/structural-context-repository.js +472 -0
  96. package/core/infrastructure/structural-context-utils.js +51 -0
  97. package/core/infrastructure/structural-graph-signals.js +121 -0
  98. package/core/infrastructure/structural-qualified-resolution.js +15 -0
  99. package/core/infrastructure/structural-source-definitions.js +100 -0
  100. package/core/infrastructure/tombstone-bitmap-reader.js +139 -0
  101. package/core/infrastructure/tree-sitter-provider.js +811 -37
  102. package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +50 -0
  103. package/core/query/query-router.js +55 -5
  104. package/core/ranking/file-kind-ranking.js +2192 -15
  105. package/core/ranking/late-interaction-index.js +87 -12
  106. package/core/search/cli-decoration.js +290 -0
  107. package/core/search/context-expander.js +988 -78
  108. package/core/search/index.js +1 -0
  109. package/core/search/output-policy.js +275 -0
  110. package/core/search/search-anchor.js +499 -0
  111. package/core/search/search-boost.js +93 -1
  112. package/core/search/search-cli.js +61 -204
  113. package/core/search/search-hybrid.js +250 -10
  114. package/core/search/search-pattern-chunks.js +57 -8
  115. package/core/search/search-pattern-planner.js +68 -9
  116. package/core/search/search-pattern-prefilter.js +30 -10
  117. package/core/search/search-pattern-ripgrep.js +40 -4
  118. package/core/search/search-pattern-sparse-overlay.js +256 -0
  119. package/core/search/search-pattern.js +117 -29
  120. package/core/search/search-postprocess.js +479 -5
  121. package/core/search/search-read-semantic.js +260 -23
  122. package/core/search/search-read.js +82 -64
  123. package/core/search/search-reader-pin.js +71 -0
  124. package/core/search/search-rrf.js +279 -0
  125. package/core/search/search-semantic.js +110 -5
  126. package/core/search/search-server.js +130 -57
  127. package/core/search/search-trace.js +107 -0
  128. package/core/search/server-identity.js +93 -0
  129. package/core/search/session-daemon-prewarm.mjs +33 -10
  130. package/core/search/sweet-search.js +399 -7
  131. package/core/skills/sweet-index/SKILL.md +8 -6
  132. package/core/vector-store/binary-hnsw-index.js +194 -30
  133. package/core/vector-store/float-vector-store.js +96 -6
  134. package/core/vector-store/hnsw-index.js +220 -49
  135. package/eval/agent-read-workflows/bin/_ss-helpers.mjs +471 -0
  136. package/eval/agent-read-workflows/bin/ss-find +15 -0
  137. package/eval/agent-read-workflows/bin/ss-grep +12 -0
  138. package/eval/agent-read-workflows/bin/ss-read +14 -0
  139. package/eval/agent-read-workflows/bin/ss-search +18 -0
  140. package/eval/agent-read-workflows/bin/ss-semantic +12 -0
  141. package/eval/agent-read-workflows/bin/ss-trace +11 -0
  142. package/mcp/read-tool.js +109 -0
  143. package/mcp/server.js +55 -15
  144. package/mcp/tool-handlers.js +14 -124
  145. package/mcp/trace-tool.js +81 -0
  146. package/package.json +25 -10
  147. package/scripts/hooks/intercept-read.mjs +55 -0
  148. package/scripts/hooks/remind-tools.mjs +40 -0
  149. package/scripts/init.js +698 -54
  150. package/scripts/inject-agent-instructions.js +431 -0
  151. package/scripts/install-prompt-reminders.js +188 -0
  152. package/scripts/install-tool-enforcement.js +220 -0
  153. package/scripts/smoke-test.js +12 -9
  154. package/scripts/uninstall.js +276 -18
  155. 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
+ }