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
@@ -15,10 +15,11 @@
15
15
 
16
16
  import { existsSync } from 'fs';
17
17
  import { DB_PATHS, GRAPH_CONFIG } from '../infrastructure/config/index.js';
18
- import { applyReadPragmas } from '../infrastructure/db-utils.js';
18
+ import { applyReadPragmas, assertInClauseSize } from '../infrastructure/db-utils.js';
19
19
  import { detectIntent, getIntentBoost } from '../query/intent-detector.js';
20
20
  import { applyMMR, shouldApplyMMR } from '../ranking/mmr.js';
21
21
  import { SYMBOL_KIND_WEIGHTS, DEFINITION_TYPES } from '../infrastructure/constants.js';
22
+ import { readAdjacentManifest, resolveManifestCodeGraphPath, sqlAliasPrefix } from '../infrastructure/code-graph-visibility.js';
22
23
 
23
24
  // Fix 9: Abbreviation expansion dictionary for common software abbreviations
24
25
  const ABBREVIATION_EXPANSIONS = {
@@ -44,17 +45,54 @@ const ABBREVIATION_EXPANSIONS = {
44
45
  // =============================================================================
45
46
 
46
47
  export class GraphSearch {
47
- constructor(dbPath = DB_PATHS.codeGraph) {
48
- this.dbPath = dbPath;
48
+ constructor(dbPath = DB_PATHS.codeGraph, options = {}) {
49
+ this._baseDbPath = dbPath;
50
+ this._explicitManifestEpoch = Number.isInteger(options.manifestEpoch);
51
+ this._manifestEpoch = this._explicitManifestEpoch ? options.manifestEpoch : null;
52
+ const manifest = this._explicitManifestEpoch ? readAdjacentManifest(this._baseDbPath) : null;
53
+ this.dbPath = this._explicitManifestEpoch
54
+ ? resolveManifestCodeGraphPath(this._baseDbPath, manifest)
55
+ : dbPath;
49
56
  this.db = null;
50
57
  this.hasFts5 = false;
51
58
  this.hasTrigram = false;
59
+ this._hasEntityEpochVisibility = false;
60
+ this._hasRelationshipEpochVisibility = false;
61
+ this._hasHcgsSummaryMetadata = false;
62
+ this._summaryVisibilityCache = new Map();
63
+ if (!this._explicitManifestEpoch) {
64
+ this._syncAdjacentManifest();
65
+ }
66
+ }
67
+
68
+ _syncAdjacentManifest() {
69
+ if (this._explicitManifestEpoch) return false;
70
+ const manifest = readAdjacentManifest(this._baseDbPath);
71
+ const nextEpoch = Number.isInteger(manifest?.epoch) ? manifest.epoch : null;
72
+ const nextDbPath = resolveManifestCodeGraphPath(this._baseDbPath, manifest);
73
+ const changed = nextEpoch !== this._manifestEpoch || nextDbPath !== this.dbPath;
74
+ this._manifestEpoch = nextEpoch;
75
+ this.dbPath = nextDbPath;
76
+ if (changed) {
77
+ this.close();
78
+ }
79
+ return changed;
80
+ }
81
+
82
+ refreshManifestEpoch() {
83
+ this._syncAdjacentManifest();
84
+ return this._manifestEpoch;
85
+ }
86
+
87
+ getManifestEpoch() {
88
+ return this._manifestEpoch;
52
89
  }
53
90
 
54
91
  /**
55
92
  * Initialize database connection (synchronous with better-sqlite3)
56
93
  */
57
94
  async init() {
95
+ this._syncAdjacentManifest();
58
96
  if (this.db) return;
59
97
 
60
98
  if (!existsSync(this.dbPath)) {
@@ -80,6 +118,16 @@ export class GraphSearch {
80
118
  this.hasTrigram = false;
81
119
  }
82
120
 
121
+ const entityCols = db.prepare('PRAGMA table_info(entities)').all().map((c) => c.name);
122
+ this._hasEntityEpochVisibility = entityCols.includes('epoch_written')
123
+ && entityCols.includes('epoch_retired');
124
+ const relCols = db.prepare('PRAGMA table_info(relationships)').all().map((c) => c.name);
125
+ this._hasRelationshipEpochVisibility = relCols.includes('epoch_written')
126
+ && relCols.includes('epoch_retired');
127
+ this._hasHcgsSummaryMetadata = !!db.prepare(
128
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='hcgs_summary_metadata'",
129
+ ).get();
130
+
83
131
  // Detect FTS5 column count to warn about schema mismatches with old databases.
84
132
  // The bm25() weights are positional — wrong column count produces wrong ranking.
85
133
  if (this.hasFts5) {
@@ -102,6 +150,7 @@ export class GraphSearch {
102
150
  // Prepare failures on optional FTS paths should degrade to existing fallbacks.
103
151
  if (this.hasFts5) {
104
152
  try {
153
+ const entityVis = this._entityVisibilitySql('e');
105
154
  this._stmtFts5 = db.prepare(`
106
155
  SELECT
107
156
  e.id, e.file_path, e.type, e.name, e.signature,
@@ -110,7 +159,7 @@ export class GraphSearch {
110
159
  FROM entities_fts
111
160
  JOIN entities e ON entities_fts.rowid = e.rowid
112
161
  WHERE entities_fts MATCH ?
113
- AND e.stale_since IS NULL
162
+ AND ${entityVis}
114
163
  ORDER BY score
115
164
  LIMIT ?
116
165
  `);
@@ -122,6 +171,7 @@ export class GraphSearch {
122
171
 
123
172
  if (this.hasTrigram) {
124
173
  try {
174
+ const entityVis = this._entityVisibilitySql('e');
125
175
  this._stmtTrigram = db.prepare(`
126
176
  SELECT
127
177
  e.id, e.file_path, e.type, e.name, e.signature,
@@ -130,7 +180,7 @@ export class GraphSearch {
130
180
  FROM entities_trigram
131
181
  JOIN entities e ON entities_trigram.rowid = e.rowid
132
182
  WHERE entities_trigram MATCH ?
133
- AND e.stale_since IS NULL
183
+ AND ${entityVis}
134
184
  ORDER BY score
135
185
  LIMIT ?
136
186
  `);
@@ -141,15 +191,19 @@ export class GraphSearch {
141
191
  }
142
192
 
143
193
  this._stmtEntityById = db.prepare(
144
- 'SELECT * FROM entities WHERE id = ? AND stale_since IS NULL'
194
+ `SELECT * FROM entities WHERE id = ? AND ${this._entityVisibilitySql('')}`
145
195
  );
146
196
 
147
197
  this._stmtOutRels = db.prepare(`
148
198
  SELECT e.*, r.type as rel_type, r.weight as rel_weight
149
199
  FROM relationships r
150
- JOIN entities e ON e.id = r.target_id OR e.name = r.target_name
200
+ JOIN entities e ON (
201
+ (r.target_id IS NOT NULL AND e.id = r.target_id)
202
+ OR (r.target_id IS NULL AND e.name = r.target_name)
203
+ )
151
204
  WHERE r.source_id = ?
152
- AND e.stale_since IS NULL
205
+ AND ${this._entityVisibilitySql('e')}
206
+ AND ${this._relationshipVisibilitySql('r')}
153
207
  LIMIT 20
154
208
  `);
155
209
 
@@ -158,7 +212,8 @@ export class GraphSearch {
158
212
  FROM relationships r
159
213
  JOIN entities e ON e.id = r.source_id
160
214
  WHERE (r.target_id = ? OR r.target_name = (SELECT name FROM entities WHERE id = ?))
161
- AND e.stale_since IS NULL
215
+ AND ${this._entityVisibilitySql('e')}
216
+ AND ${this._relationshipVisibilitySql('r')}
162
217
  LIMIT 20
163
218
  `);
164
219
 
@@ -176,6 +231,107 @@ export class GraphSearch {
176
231
  }
177
232
  }
178
233
 
234
+ _entityVisibilitySql(alias = 'e', options = {}) {
235
+ const prefix = sqlAliasPrefix(alias);
236
+ let sql;
237
+ if (!this._hasEntityEpochVisibility) {
238
+ sql = `${prefix}stale_since IS NULL`;
239
+ } else if (this._manifestEpoch !== null) {
240
+ // For a pinned reader, a row retired after the pinned epoch remains
241
+ // visible even if the compatibility stale_since flag has been set.
242
+ sql = `(${prefix}epoch_written IS NULL OR ${prefix}epoch_written <= ?)
243
+ AND (${prefix}epoch_retired IS NULL OR ${prefix}epoch_retired > ?)
244
+ AND (${prefix}stale_since IS NULL OR (${prefix}epoch_retired IS NOT NULL AND ${prefix}epoch_retired > ?))`;
245
+ } else {
246
+ sql = `${prefix}stale_since IS NULL AND ${prefix}epoch_retired IS NULL`;
247
+ }
248
+ if (options.allowNullJoined) {
249
+ return `(${sql} OR ${prefix}id IS NULL)`;
250
+ }
251
+ return sql;
252
+ }
253
+
254
+ _entityVisibilityParams() {
255
+ if (!this._hasEntityEpochVisibility || this._manifestEpoch === null) return [];
256
+ return [this._manifestEpoch, this._manifestEpoch, this._manifestEpoch];
257
+ }
258
+
259
+ _relationshipVisibilitySql(alias = 'r') {
260
+ if (!this._hasRelationshipEpochVisibility) return '1=1';
261
+ const prefix = sqlAliasPrefix(alias);
262
+ if (this._manifestEpoch !== null) {
263
+ return `(${prefix}epoch_written IS NULL OR ${prefix}epoch_written <= ?)
264
+ AND (${prefix}epoch_retired IS NULL OR ${prefix}epoch_retired > ?)`;
265
+ }
266
+ return `${prefix}epoch_retired IS NULL`;
267
+ }
268
+
269
+ _relationshipVisibilityParams() {
270
+ if (!this._hasRelationshipEpochVisibility || this._manifestEpoch === null) return [];
271
+ return [this._manifestEpoch, this._manifestEpoch];
272
+ }
273
+
274
+ _summaryJoinSql(entityAlias = '') {
275
+ if (!this._hasHcgsSummaryMetadata) return '';
276
+ const prefix = sqlAliasPrefix(entityAlias);
277
+ return `LEFT JOIN hcgs_summary_metadata hm ON hm.entity_id = ${prefix}id`;
278
+ }
279
+
280
+ _summarySelectSql(entityAlias = '') {
281
+ const prefix = sqlAliasPrefix(entityAlias);
282
+ if (!this._hasHcgsSummaryMetadata) return `${prefix}summary`;
283
+ if (this._manifestEpoch !== null) {
284
+ return `CASE WHEN hm.entity_id IS NOT NULL
285
+ AND (hm.epoch_written IS NULL OR hm.epoch_written <= ?)
286
+ AND (hm.epoch_retired IS NULL OR hm.epoch_retired > ?)
287
+ THEN ${prefix}summary ELSE NULL END AS summary`;
288
+ }
289
+ return `CASE WHEN hm.entity_id IS NOT NULL AND hm.epoch_retired IS NULL
290
+ THEN ${prefix}summary ELSE NULL END AS summary`;
291
+ }
292
+
293
+ _summaryVisibilityParams() {
294
+ if (!this._hasHcgsSummaryMetadata || this._manifestEpoch === null) return [];
295
+ return [this._manifestEpoch, this._manifestEpoch];
296
+ }
297
+
298
+ _summaryVisibleForEntity(entityId) {
299
+ if (!this._hasHcgsSummaryMetadata && this.db) {
300
+ this._hasHcgsSummaryMetadata = !!this.db.prepare(
301
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='hcgs_summary_metadata'",
302
+ ).get();
303
+ }
304
+ if (!this._hasHcgsSummaryMetadata || entityId == null) return true;
305
+ if (this._manifestEpoch === null) {
306
+ const row = this.db.prepare(`
307
+ SELECT epoch_retired
308
+ FROM hcgs_summary_metadata
309
+ WHERE entity_id = ?
310
+ `).get(String(entityId));
311
+ return row ? row.epoch_retired == null : false;
312
+ }
313
+ const key = `${entityId}:${this._manifestEpoch ?? 'live'}`;
314
+ if (this._summaryVisibilityCache.has(key)) return this._summaryVisibilityCache.get(key);
315
+ const row = this.db.prepare(`
316
+ SELECT epoch_written, epoch_retired
317
+ FROM hcgs_summary_metadata
318
+ WHERE entity_id = ?
319
+ `).get(String(entityId));
320
+ const visible = row
321
+ ? (this._manifestEpoch !== null
322
+ ? (row.epoch_written == null || row.epoch_written <= this._manifestEpoch)
323
+ && (row.epoch_retired == null || row.epoch_retired > this._manifestEpoch)
324
+ : row.epoch_retired == null)
325
+ : false;
326
+ this._summaryVisibilityCache.set(key, visible);
327
+ return visible;
328
+ }
329
+
330
+ _rowWithVisibleSummary(row) {
331
+ if (!row || row.summary == null || this._summaryVisibleForEntity(row.id)) return row;
332
+ return { ...row, summary: null };
333
+ }
334
+
179
335
  /**
180
336
  * Map a raw FTS5/trigram/LIKE row to a search result object.
181
337
  */
@@ -223,14 +379,14 @@ export class GraphSearch {
223
379
  start_line, end_line, package, parent_class, search_text
224
380
  FROM entities
225
381
  WHERE ${likeConditions || '1=1'}
226
- AND stale_since IS NULL
382
+ AND ${this._entityVisibilitySql('')}
227
383
  ORDER BY
228
384
  CASE WHEN name LIKE ? THEN 0 ELSE 1 END,
229
385
  length(name)
230
386
  LIMIT ?
231
387
  `);
232
388
 
233
- const rows = stmt.all(...likeParams, `%${query}%`, limit);
389
+ const rows = stmt.all(...likeParams, ...this._entityVisibilityParams(), `%${query}%`, limit);
234
390
 
235
391
  let rank = 0;
236
392
  for (const row of rows) {
@@ -287,13 +443,18 @@ export class GraphSearch {
287
443
  await this.init();
288
444
  if (!this._stmtEntityByName) {
289
445
  this._stmtEntityByName = this.db.prepare(`
290
- SELECT id, name, type, signature, summary, file_path, start_line
446
+ SELECT id, name, type, signature, ${this._summarySelectSql('entities')}, file_path, start_line
291
447
  FROM entities
292
- WHERE name = ? AND stale_since IS NULL
448
+ ${this._summaryJoinSql('entities')}
449
+ WHERE name = ? AND ${this._entityVisibilitySql('entities')}
293
450
  LIMIT 1
294
451
  `);
295
452
  }
296
- return this._stmtEntityByName.get(name) || null;
453
+ return this._rowWithVisibleSummary(this._stmtEntityByName.get(
454
+ ...this._summaryVisibilityParams(),
455
+ name,
456
+ ...this._entityVisibilityParams(),
457
+ )) || null;
297
458
  }
298
459
 
299
460
  /**
@@ -304,14 +465,21 @@ export class GraphSearch {
304
465
  await this.init();
305
466
  if (!this._stmtEntityByLocation) {
306
467
  this._stmtEntityByLocation = this.db.prepare(`
307
- SELECT id, name, type, signature, summary, file_path, start_line
468
+ SELECT id, name, type, signature, ${this._summarySelectSql('entities')}, file_path, start_line
308
469
  FROM entities
470
+ ${this._summaryJoinSql('entities')}
309
471
  WHERE file_path LIKE ? AND start_line <= ? AND end_line >= ?
310
- AND stale_since IS NULL
472
+ AND ${this._entityVisibilitySql('entities')}
311
473
  LIMIT 1
312
474
  `);
313
475
  }
314
- return this._stmtEntityByLocation.get(`%${filePath}%`, line, line) || null;
476
+ return this._rowWithVisibleSummary(this._stmtEntityByLocation.get(
477
+ ...this._summaryVisibilityParams(),
478
+ `%${filePath}%`,
479
+ line,
480
+ line,
481
+ ...this._entityVisibilityParams(),
482
+ )) || null;
315
483
  }
316
484
 
317
485
  /**
@@ -340,14 +508,19 @@ export class GraphSearch {
340
508
  if (names.size > 0) {
341
509
  if (!this._stmtEntityByName) {
342
510
  this._stmtEntityByName = this.db.prepare(`
343
- SELECT id, name, type, signature, summary, file_path, start_line
511
+ SELECT id, name, type, signature, ${this._summarySelectSql('entities')}, file_path, start_line
344
512
  FROM entities
345
- WHERE name = ? AND stale_since IS NULL
513
+ ${this._summaryJoinSql('entities')}
514
+ WHERE name = ? AND ${this._entityVisibilitySql('entities')}
346
515
  LIMIT 1
347
516
  `);
348
517
  }
349
518
  for (const name of names) {
350
- const entity = this._stmtEntityByName.get(name);
519
+ const entity = this._rowWithVisibleSummary(this._stmtEntityByName.get(
520
+ ...this._summaryVisibilityParams(),
521
+ name,
522
+ ...this._entityVisibilityParams(),
523
+ ));
351
524
  if (entity) byName.set(name, entity);
352
525
  }
353
526
  }
@@ -356,17 +529,24 @@ export class GraphSearch {
356
529
  if (locations.length > 0) {
357
530
  if (!this._stmtEntityByLocation) {
358
531
  this._stmtEntityByLocation = this.db.prepare(`
359
- SELECT id, name, type, signature, summary, file_path, start_line
532
+ SELECT id, name, type, signature, ${this._summarySelectSql('entities')}, file_path, start_line
360
533
  FROM entities
534
+ ${this._summaryJoinSql('entities')}
361
535
  WHERE file_path LIKE ? AND start_line <= ? AND end_line >= ?
362
- AND stale_since IS NULL
536
+ AND ${this._entityVisibilitySql('entities')}
363
537
  LIMIT 1
364
538
  `);
365
539
  }
366
540
  for (const { file, line } of locations) {
367
541
  const key = `${file}:${line}`;
368
542
  if (byLocation.has(key)) continue;
369
- const entity = this._stmtEntityByLocation.get(`%${file}%`, line, line);
543
+ const entity = this._rowWithVisibleSummary(this._stmtEntityByLocation.get(
544
+ ...this._summaryVisibilityParams(),
545
+ `%${file}%`,
546
+ line,
547
+ line,
548
+ ...this._entityVisibilityParams(),
549
+ ));
370
550
  if (entity) byLocation.set(key, entity);
371
551
  }
372
552
  }
@@ -382,7 +562,14 @@ export class GraphSearch {
382
562
  await this.init();
383
563
  const result = new Set();
384
564
  try {
385
- const rows = this.db.prepare('SELECT id FROM entities WHERE stale_since IS NOT NULL').all();
565
+ const rows = this._hasEntityEpochVisibility && this._manifestEpoch !== null
566
+ ? this.db.prepare(`
567
+ SELECT id FROM entities
568
+ WHERE stale_since IS NOT NULL
569
+ AND (epoch_written IS NULL OR epoch_written <= ?)
570
+ AND (epoch_retired IS NULL OR epoch_retired <= ?)
571
+ `).all(this._manifestEpoch, this._manifestEpoch)
572
+ : this.db.prepare('SELECT id FROM entities WHERE stale_since IS NOT NULL').all();
386
573
  for (const row of rows) result.add(row.id);
387
574
  } catch {
388
575
  // Column may not exist in older indexes
@@ -418,6 +605,7 @@ export class GraphSearch {
418
605
  this._stmtInRels = null;
419
606
  this._stmtEntityByName = null;
420
607
  this._stmtEntityByLocation = null;
608
+ this._summaryVisibilityCache = new Map();
421
609
  if (this.db) {
422
610
  this.db.close();
423
611
  this.db = null;
@@ -452,7 +640,11 @@ export class GraphSearch {
452
640
  if (useNameRestriction) {
453
641
  restrictedAttempted = true;
454
642
  try {
455
- rows = this._stmtFts5.all(`name : ${this.sanitizeFtsQuery(query)}`, limit);
643
+ rows = this._stmtFts5.all(
644
+ `name : ${this.sanitizeFtsQuery(query)}`,
645
+ ...this._entityVisibilityParams(),
646
+ limit,
647
+ );
456
648
  } catch (err) {
457
649
  this.log(`[bm25Search] Name-restricted FTS5 query failed: ${err.message}`);
458
650
  rows = [];
@@ -463,7 +655,11 @@ export class GraphSearch {
463
655
  if (rows.length === 0) {
464
656
  restrictedFallback = restrictedAttempted;
465
657
  try {
466
- rows = this._stmtFts5.all(this.sanitizeFtsQuery(query), limit);
658
+ rows = this._stmtFts5.all(
659
+ this.sanitizeFtsQuery(query),
660
+ ...this._entityVisibilityParams(),
661
+ limit,
662
+ );
467
663
  } catch (err) {
468
664
  this.log(`[bm25Search] FTS5 query failed: ${err.message}`);
469
665
  rows = [];
@@ -481,7 +677,11 @@ export class GraphSearch {
481
677
  try {
482
678
  // Trigram search - just use the raw query (trigram handles substrings)
483
679
  const escaped = query.replace(/"/g, '""');
484
- const trigramRows = this._stmtTrigram.all(`"${escaped}"`, limit);
680
+ const trigramRows = this._stmtTrigram.all(
681
+ `"${escaped}"`,
682
+ ...this._entityVisibilityParams(),
683
+ limit,
684
+ );
485
685
 
486
686
  this._mergeRows(results, trigramRows, 'trigram', 0.9);
487
687
  } catch (err) {
@@ -494,7 +694,11 @@ export class GraphSearch {
494
694
  try {
495
695
  const expandedForm = this.expandIdentifierQuery(query);
496
696
  if (expandedForm && expandedForm !== this.sanitizeFtsQuery(query)) {
497
- const expandedRows = this._stmtFts5.all(expandedForm, limit);
697
+ const expandedRows = this._stmtFts5.all(
698
+ expandedForm,
699
+ ...this._entityVisibilityParams(),
700
+ limit,
701
+ );
498
702
  this._mergeRows(results, expandedRows, 'fts5_expanded', 0.85);
499
703
  }
500
704
  } catch (err) {
@@ -507,7 +711,11 @@ export class GraphSearch {
507
711
  try {
508
712
  const abbrQuery = this.expandAbbreviations(query);
509
713
  if (abbrQuery) {
510
- const abbrRows = this._stmtFts5.all(abbrQuery, limit);
714
+ const abbrRows = this._stmtFts5.all(
715
+ abbrQuery,
716
+ ...this._entityVisibilityParams(),
717
+ limit,
718
+ );
511
719
  this._mergeRows(results, abbrRows, 'fts5_abbr', 0.8);
512
720
  }
513
721
  } catch (err) {
@@ -559,7 +767,11 @@ export class GraphSearch {
559
767
 
560
768
  if (useNameRestriction) {
561
769
  try {
562
- rows = this._stmtFts5.all(`name : ${this.sanitizeFtsQuery(query)}`, limit);
770
+ rows = this._stmtFts5.all(
771
+ `name : ${this.sanitizeFtsQuery(query)}`,
772
+ ...this._entityVisibilityParams(),
773
+ limit,
774
+ );
563
775
  } catch (err) {
564
776
  this.log(`[bm25SearchRaw] Name-restricted FTS5 query failed: ${err.message}`);
565
777
  rows = [];
@@ -568,7 +780,11 @@ export class GraphSearch {
568
780
 
569
781
  if (rows.length === 0) {
570
782
  try {
571
- rows = this._stmtFts5.all(this.sanitizeFtsQuery(query), limit);
783
+ rows = this._stmtFts5.all(
784
+ this.sanitizeFtsQuery(query),
785
+ ...this._entityVisibilityParams(),
786
+ limit,
787
+ );
572
788
  } catch (err) {
573
789
  this.log(`[bm25SearchRaw] FTS5 query failed: ${err.message}`);
574
790
  rows = [];
@@ -584,7 +800,11 @@ export class GraphSearch {
584
800
  if (this.hasTrigram && results.length < 3 && query.length >= 3) {
585
801
  try {
586
802
  const escapedRaw = query.replace(/"/g, '""');
587
- const trigramRows = this._stmtTrigram.all(`"${escapedRaw}"`, limit);
803
+ const trigramRows = this._stmtTrigram.all(
804
+ `"${escapedRaw}"`,
805
+ ...this._entityVisibilityParams(),
806
+ limit,
807
+ );
588
808
  this._mergeRows(results, trigramRows, 'trigram_raw', 0.9);
589
809
  } catch (err) {
590
810
  this.log(`[bm25SearchRaw] Trigram query failed: ${err.message}`);
@@ -972,7 +1192,7 @@ export class GraphSearch {
972
1192
  async searchByName(name, type = null) {
973
1193
  await this.init();
974
1194
 
975
- let query = `SELECT * FROM entities WHERE name = ? AND stale_since IS NULL`;
1195
+ let query = 'SELECT * FROM entities WHERE name = ?';
976
1196
  const params = [name];
977
1197
 
978
1198
  if (type) {
@@ -980,10 +1200,12 @@ export class GraphSearch {
980
1200
  params.push(type);
981
1201
  }
982
1202
 
1203
+ query += ` AND ${this._entityVisibilitySql('')}`;
983
1204
  query += ' LIMIT 50';
984
1205
 
985
1206
  const stmt = this.db.prepare(query);
986
- return stmt.all(...params);
1207
+ return stmt.all(...params, ...this._entityVisibilityParams())
1208
+ .map(row => this._rowWithVisibleSummary(row));
987
1209
  }
988
1210
 
989
1211
  /**
@@ -995,12 +1217,13 @@ export class GraphSearch {
995
1217
  const stmt = this.db.prepare(`
996
1218
  SELECT * FROM entities
997
1219
  WHERE file_path LIKE ?
998
- AND stale_since IS NULL
1220
+ AND ${this._entityVisibilitySql('')}
999
1221
  ORDER BY start_line
1000
1222
  LIMIT 100
1001
1223
  `);
1002
1224
 
1003
- return stmt.all(`%${pathPattern}%`);
1225
+ return stmt.all(`%${pathPattern}%`, ...this._entityVisibilityParams())
1226
+ .map(row => this._rowWithVisibleSummary(row));
1004
1227
  }
1005
1228
 
1006
1229
  /**
@@ -1024,30 +1247,44 @@ export class GraphSearch {
1024
1247
  if (frontierIds.length === 0) break;
1025
1248
 
1026
1249
  // Single query for all entities in this hop
1250
+ assertInClauseSize(frontierIds.length, 'graph-search.frontier-fetch');
1027
1251
  const placeholders = frontierIds.map(() => '?').join(',');
1028
1252
  const entities = this.db.prepare(`
1029
1253
  SELECT * FROM entities
1030
1254
  WHERE id IN (${placeholders})
1031
- AND stale_since IS NULL
1032
- `).all(...frontierIds);
1255
+ AND ${this._entityVisibilitySql('')}
1256
+ `).all(...frontierIds, ...this._entityVisibilityParams());
1033
1257
 
1034
1258
  // Create lookup map for fast access
1035
1259
  const entityMap = new Map(entities.map(e => [e.id, e]));
1036
1260
 
1037
- // P2 FIX: Batch fetch all relationships (outgoing + incoming) in single query
1261
+ // P2 FIX: Batch fetch all relationships (outgoing + incoming) in single query.
1262
+ // The UNION ALL binds `frontierIds` twice in one prepared statement, so the
1263
+ // SQLite-parameter ceiling is reached at half the array length — guard the
1264
+ // actual bind count, not the array length.
1265
+ assertInClauseSize(2 * frontierIds.length, 'graph-search.frontier-relationships (double-bind UNION)');
1038
1266
  const relationships = this.db.prepare(`
1039
1267
  SELECT r.*, 'out' as direction, r.source_id as origin_id
1040
1268
  FROM relationships r
1041
1269
  JOIN entities e ON r.target_id = e.id
1042
1270
  WHERE r.source_id IN (${placeholders})
1043
- AND e.stale_since IS NULL
1271
+ AND ${this._entityVisibilitySql('e')}
1272
+ AND ${this._relationshipVisibilitySql('r')}
1044
1273
  UNION ALL
1045
1274
  SELECT r.*, 'in' as direction, r.target_id as origin_id
1046
1275
  FROM relationships r
1047
1276
  JOIN entities e ON r.source_id = e.id
1048
1277
  WHERE r.target_id IN (${placeholders})
1049
- AND e.stale_since IS NULL
1050
- `).all(...frontierIds, ...frontierIds);
1278
+ AND ${this._entityVisibilitySql('e')}
1279
+ AND ${this._relationshipVisibilitySql('r')}
1280
+ `).all(
1281
+ ...frontierIds,
1282
+ ...this._entityVisibilityParams(),
1283
+ ...this._relationshipVisibilityParams(),
1284
+ ...frontierIds,
1285
+ ...this._entityVisibilityParams(),
1286
+ ...this._relationshipVisibilityParams(),
1287
+ );
1051
1288
 
1052
1289
  // Group relationships by their origin entity
1053
1290
  const relMap = new Map();
@@ -1059,7 +1296,7 @@ export class GraphSearch {
1059
1296
 
1060
1297
  // Process all entities without additional queries
1061
1298
  for (const entityId of frontierIds) {
1062
- const entity = entityMap.get(entityId);
1299
+ const entity = this._rowWithVisibleSummary(entityMap.get(entityId));
1063
1300
  if (entity) {
1064
1301
  expanded.set(entityId, {
1065
1302
  ...entity,
@@ -1090,7 +1327,9 @@ export class GraphSearch {
1090
1327
  */
1091
1328
  async getEntity(entityId) {
1092
1329
  await this.init();
1093
- return this._stmtEntityById.get(entityId) || null;
1330
+ return this._rowWithVisibleSummary(
1331
+ this._stmtEntityById.get(entityId, ...this._entityVisibilityParams()),
1332
+ ) || null;
1094
1333
  }
1095
1334
 
1096
1335
  /**
@@ -1102,8 +1341,13 @@ export class GraphSearch {
1102
1341
  const results = [];
1103
1342
 
1104
1343
  // Find outgoing relationships (cached prepared statement)
1105
- const outRows = this._stmtOutRels.all(entityId);
1106
- for (const row of outRows) {
1344
+ const outRows = this._stmtOutRels.all(
1345
+ entityId,
1346
+ ...this._entityVisibilityParams(),
1347
+ ...this._relationshipVisibilityParams(),
1348
+ );
1349
+ for (const rawRow of outRows) {
1350
+ const row = this._rowWithVisibleSummary(rawRow);
1107
1351
  results.push({
1108
1352
  ...row,
1109
1353
  direction: 'outgoing',
@@ -1111,8 +1355,14 @@ export class GraphSearch {
1111
1355
  }
1112
1356
 
1113
1357
  // Find incoming relationships (cached prepared statement)
1114
- const inRows = this._stmtInRels.all(entityId, entityId);
1115
- for (const row of inRows) {
1358
+ const inRows = this._stmtInRels.all(
1359
+ entityId,
1360
+ entityId,
1361
+ ...this._entityVisibilityParams(),
1362
+ ...this._relationshipVisibilityParams(),
1363
+ );
1364
+ for (const rawRow of inRows) {
1365
+ const row = this._rowWithVisibleSummary(rawRow);
1116
1366
  results.push({
1117
1367
  ...row,
1118
1368
  direction: 'incoming',
@@ -1690,7 +1940,7 @@ export class GraphSearch {
1690
1940
  )
1691
1941
  -- Only match top-level definition types, not methods
1692
1942
  AND e.type IN ('class', 'interface', 'struct', 'enum', 'function', 'type', 'component')
1693
- AND e.stale_since IS NULL
1943
+ AND ${this._entityVisibilitySql('e')}
1694
1944
  ORDER BY
1695
1945
  match_priority,
1696
1946
  CASE e.type
@@ -1703,7 +1953,13 @@ export class GraphSearch {
1703
1953
  length(e.name)
1704
1954
  LIMIT 5
1705
1955
  `);
1706
- const rows = stmt.all(queryLower, queryLower, queryLower, queryLower);
1956
+ const rows = stmt.all(
1957
+ queryLower,
1958
+ queryLower,
1959
+ queryLower,
1960
+ queryLower,
1961
+ ...this._entityVisibilityParams(),
1962
+ );
1707
1963
  resolve(rows);
1708
1964
  } catch (err) {
1709
1965
  this.log(`Definition search error: ${err.message}`);
@@ -1786,6 +2042,34 @@ export class GraphSearch {
1786
2042
  return merged.slice(0, k);
1787
2043
  }
1788
2044
 
2045
+ /**
2046
+ * Resolve `entityName` to a single target entity row.
2047
+ *
2048
+ * Substring fallback (`name LIKE '%X%'`) is preserved so callers like
2049
+ * `findCallers("HttpClient")` still resolve when only `MyHttpClient`
2050
+ * exists in the index. But exact matches now sort first; otherwise short
2051
+ * names like `Vec` silently resolved to substring siblings like
2052
+ * `AlignedVector` because the prior `LIMIT 1` had no preference order.
2053
+ */
2054
+ _resolveStructuralTarget(entityName) {
2055
+ return this.db.prepare(`
2056
+ SELECT id, name, type, file_path FROM entities
2057
+ WHERE (name = ? OR name LIKE ?)
2058
+ AND ${this._entityVisibilitySql('')}
2059
+ ORDER BY
2060
+ CASE WHEN name = ? THEN 0 ELSE 1 END,
2061
+ CASE WHEN file_path LIKE '%/test/%' OR file_path LIKE 'test/%' OR file_path LIKE 'tests/%' THEN 1 ELSE 0 END,
2062
+ CASE type
2063
+ WHEN 'class' THEN 0 WHEN 'struct' THEN 0 WHEN 'trait' THEN 0
2064
+ WHEN 'interface' THEN 1 WHEN 'enum' THEN 1 WHEN 'type' THEN 1 WHEN 'typeAlias' THEN 1
2065
+ WHEN 'function' THEN 2 WHEN 'method' THEN 2
2066
+ ELSE 3
2067
+ END,
2068
+ length(name) ASC
2069
+ LIMIT 1
2070
+ `).get(entityName, `%${entityName}%`, ...this._entityVisibilityParams(), entityName);
2071
+ }
2072
+
1789
2073
  /**
1790
2074
  * Find all callers of a given entity
1791
2075
  */
@@ -1793,13 +2077,7 @@ export class GraphSearch {
1793
2077
  await this.init();
1794
2078
  const { maxDepth = 2, limit = 50 } = options;
1795
2079
 
1796
- // Find target entity
1797
- const target = this.db.prepare(`
1798
- SELECT id, name, type, file_path FROM entities
1799
- WHERE (name = ? OR name LIKE ?)
1800
- AND stale_since IS NULL
1801
- LIMIT 1
1802
- `).get(entityName, `%${entityName}%`);
2080
+ const target = this._resolveStructuralTarget(entityName);
1803
2081
 
1804
2082
  if (!target) return { results: [], stats: { found: false, query: entityName } };
1805
2083
 
@@ -1821,14 +2099,22 @@ export class GraphSearch {
1821
2099
  OR r.target_name LIKE ?
1822
2100
  OR r.target_name LIKE ?
1823
2101
  )
1824
- AND e.stale_since IS NULL
2102
+ AND ${this._entityVisibilitySql('e')}
2103
+ AND ${this._relationshipVisibilitySql('r')}
1825
2104
  ORDER BY e.file_path, r.context_line
1826
2105
  LIMIT ?
1827
- `).all(target.id, targetNamePattern, `${target.name}.%`, limit);
2106
+ `).all(
2107
+ target.id,
2108
+ targetNamePattern,
2109
+ `${target.name}.%`,
2110
+ ...this._entityVisibilityParams(),
2111
+ ...this._relationshipVisibilityParams(),
2112
+ limit,
2113
+ );
1828
2114
 
1829
2115
  return {
1830
2116
  results: callers.map(c => ({
1831
- ...c,
2117
+ ...this._rowWithVisibleSummary(c),
1832
2118
  relationship: 'calls',
1833
2119
  depth: 1,
1834
2120
  })),
@@ -1843,12 +2129,7 @@ export class GraphSearch {
1843
2129
  await this.init();
1844
2130
  const { limit = 50 } = options;
1845
2131
 
1846
- const source = this.db.prepare(`
1847
- SELECT id, name, type FROM entities
1848
- WHERE (name = ? OR name LIKE ?)
1849
- AND stale_since IS NULL
1850
- LIMIT 1
1851
- `).get(entityName, `%${entityName}%`);
2132
+ const source = this._resolveStructuralTarget(entityName);
1852
2133
 
1853
2134
  if (!source) return { results: [], stats: { found: false } };
1854
2135
 
@@ -1859,22 +2140,31 @@ export class GraphSearch {
1859
2140
  FROM relationships r
1860
2141
  LEFT JOIN entities e ON e.id = r.target_id OR e.name = r.target_name
1861
2142
  WHERE r.source_id = ? AND r.type = 'calls'
1862
- AND (e.stale_since IS NULL OR e.id IS NULL)
2143
+ AND ${this._entityVisibilitySql('e', { allowNullJoined: true })}
2144
+ AND ${this._relationshipVisibilitySql('r')}
1863
2145
  ORDER BY r.context_line
1864
2146
  LIMIT ?
1865
- `).all(source.id, limit);
2147
+ `).all(
2148
+ source.id,
2149
+ ...this._entityVisibilityParams(),
2150
+ ...this._relationshipVisibilityParams(),
2151
+ limit,
2152
+ );
1866
2153
 
1867
2154
  return {
1868
- results: callees.map(c => ({
1869
- id: c.id,
1870
- name: c.name || c.target_name,
1871
- type: c.type || 'external',
1872
- file_path: c.file_path,
1873
- start_line: c.start_line,
1874
- signature: c.signature,
1875
- call_line: c.call_line,
1876
- relationship: 'calls',
1877
- })),
2155
+ results: callees.map(c => {
2156
+ const row = this._rowWithVisibleSummary(c);
2157
+ return {
2158
+ id: row.id,
2159
+ name: row.name || row.target_name,
2160
+ type: row.type || 'external',
2161
+ file_path: row.file_path,
2162
+ start_line: row.start_line,
2163
+ signature: row.signature,
2164
+ call_line: row.call_line,
2165
+ relationship: 'calls',
2166
+ };
2167
+ }),
1878
2168
  stats: { sourceEntity: source.name, totalCallees: callees.length },
1879
2169
  };
1880
2170
  }
@@ -1894,13 +2184,23 @@ export class GraphSearch {
1894
2184
  JOIN entities e ON e.id = r.source_id
1895
2185
  WHERE (r.target_name = ? OR r.target_name LIKE ?)
1896
2186
  AND r.type IN ('implements', 'extends')
1897
- AND e.stale_since IS NULL
2187
+ AND ${this._entityVisibilitySql('e')}
2188
+ AND ${this._relationshipVisibilitySql('r')}
1898
2189
  ORDER BY e.name
1899
2190
  LIMIT ?
1900
- `).all(interfaceName, `%${interfaceName}%`, limit);
2191
+ `).all(
2192
+ interfaceName,
2193
+ `%${interfaceName}%`,
2194
+ ...this._entityVisibilityParams(),
2195
+ ...this._relationshipVisibilityParams(),
2196
+ limit,
2197
+ );
1901
2198
 
1902
2199
  return {
1903
- results: implementations.map(i => ({ ...i, relationship: i.rel_type })),
2200
+ results: implementations.map(i => ({
2201
+ ...this._rowWithVisibleSummary(i),
2202
+ relationship: i.rel_type,
2203
+ })),
1904
2204
  stats: { targetInterface: interfaceName, totalImplementations: implementations.length },
1905
2205
  };
1906
2206
  }
@@ -1912,12 +2212,7 @@ export class GraphSearch {
1912
2212
  await this.init();
1913
2213
  const { maxDepth = 3, limit = 100 } = options;
1914
2214
 
1915
- const target = this.db.prepare(`
1916
- SELECT id, name, type FROM entities
1917
- WHERE (name = ? OR name LIKE ?)
1918
- AND stale_since IS NULL
1919
- LIMIT 1
1920
- `).get(entityName, `%${entityName}%`);
2215
+ const target = this._resolveStructuralTarget(entityName);
1921
2216
 
1922
2217
  if (!target) return { results: [], stats: { found: false } };
1923
2218
 
@@ -1930,6 +2225,7 @@ export class GraphSearch {
1930
2225
  for (let depth = 1; depth <= maxDepth && impacted.size < limit; depth++) {
1931
2226
  if (frontier.length === 0) break;
1932
2227
 
2228
+ assertInClauseSize(frontier.length, 'graph-search.impact-frontier');
1933
2229
  const placeholders = frontier.map(() => '?').join(',');
1934
2230
  let dependents;
1935
2231
 
@@ -1942,9 +2238,15 @@ export class GraphSearch {
1942
2238
  FROM relationships r
1943
2239
  JOIN entities e ON e.id = r.source_id
1944
2240
  WHERE (r.target_id IN (${placeholders}) OR r.target_name LIKE ?)
1945
- AND e.stale_since IS NULL
2241
+ AND ${this._entityVisibilitySql('e')}
2242
+ AND ${this._relationshipVisibilitySql('r')}
1946
2243
  LIMIT 50
1947
- `).all(...frontier, targetNamePattern);
2244
+ `).all(
2245
+ ...frontier,
2246
+ targetNamePattern,
2247
+ ...this._entityVisibilityParams(),
2248
+ ...this._relationshipVisibilityParams(),
2249
+ );
1948
2250
  } else {
1949
2251
  // Subsequent depths: only match by target_id
1950
2252
  dependents = this.db.prepare(`
@@ -1954,16 +2256,17 @@ export class GraphSearch {
1954
2256
  FROM relationships r
1955
2257
  JOIN entities e ON e.id = r.source_id
1956
2258
  WHERE r.target_id IN (${placeholders})
1957
- AND e.stale_since IS NULL
2259
+ AND ${this._entityVisibilitySql('e')}
2260
+ AND ${this._relationshipVisibilitySql('r')}
1958
2261
  LIMIT 50
1959
- `).all(...frontier);
2262
+ `).all(...frontier, ...this._entityVisibilityParams(), ...this._relationshipVisibilityParams());
1960
2263
  }
1961
2264
 
1962
2265
  const nextFrontier = [];
1963
2266
  for (const dep of dependents) {
1964
2267
  if (!impacted.has(dep.id) && dep.id !== target.id) {
1965
2268
  impacted.set(dep.id, {
1966
- ...dep,
2269
+ ...this._rowWithVisibleSummary(dep),
1967
2270
  relationship: dep.rel_type,
1968
2271
  depth,
1969
2272
  riskScore: (4 - depth) / 3, // Higher for closer dependencies