@zokizuan/satori-mcp 3.5.0 → 3.8.0

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/README.md CHANGED
@@ -45,6 +45,10 @@ Tool surface is hard-broken to 6 tools. This keeps routing explicit while exposi
45
45
  - Enabled by default. Set `MCP_ENABLE_WATCHER=false` to disable
46
46
  - Debounce window via `MCP_WATCH_DEBOUNCE_MS` (default `5000`)
47
47
  - Watch events reuse the same incremental sync pipeline (`reindexByChange`)
48
+ - Ignore control files (`.satoriignore`, root `.gitignore`) trigger no-reindex reconciliation:
49
+ - delete indexed paths now ignored by active rules
50
+ - incremental sync picks up newly unignored files
51
+ - signature checks in `ensureFreshness` keep this working even when watcher events are missed
48
52
  - Safety gates:
49
53
  - Watch-triggered sync only runs for `indexed`/`sync_completed` codebases
50
54
  - Events are dropped for `indexing`, `indexfailed`, and `requires_reindex`
@@ -57,7 +61,7 @@ Tool surface is hard-broken to 6 tools. This keeps routing explicit while exposi
57
61
 
58
62
  ### `manage_index`
59
63
 
60
- Manage index lifecycle operations (create/reindex/sync/status/clear) for a codebase path.
64
+ Manage index lifecycle operations (create/reindex/sync/status/clear) for a codebase path. Ignore-rule edits in repo-root .satoriignore/.gitignore reconcile automatically in the normal sync path (no full reindex required). If you need immediate convergence after ignore edits, run action="sync" and then rerun search_codebase. Use action="reindex" for fingerprint/schema incompatibility or full rebuild recovery.
61
65
 
62
66
  | Parameter | Type | Required | Default | Description |
63
67
  |---|---|---|---|---|
@@ -71,7 +75,7 @@ Manage index lifecycle operations (create/reindex/sync/status/clear) for a codeb
71
75
 
72
76
  ### `search_codebase`
73
77
 
74
- Unified semantic search with runtime/docs scope control, grouped/raw output modes, deterministic ranking, and structured freshness decision.
78
+ Unified semantic search with runtime/docs scope control, grouped/raw output modes, deterministic ranking, and structured freshness decisions. For runtime debugging, start with scope="runtime". If you need both runtime and docs context, use scope="mixed". If top results are dominated by tests/fixtures/docs, edit repo-root .satoriignore using your host/editor (examples, not exhaustive: **/*.test.*, **/*.spec.*, **/__tests__/**, **/__fixtures__/**, **/fixtures/**, coverage/**), wait one debounce window (MCP_WATCH_DEBOUNCE_MS, default 5000ms), then rerun search_codebase. For immediate convergence, run manage_index with {"action":"sync","path":"<same path used in search_codebase>"}.
75
79
 
76
80
  | Parameter | Type | Required | Default | Description |
77
81
  |---|---|---|---|---|
@@ -106,6 +110,9 @@ Return a sidecar-backed symbol outline for one file, including call_graph jump h
106
110
  | `start_line` | integer | no | | Optional start line filter (1-based, inclusive). |
107
111
  | `end_line` | integer | no | | Optional end line filter (1-based, inclusive). |
108
112
  | `limitSymbols` | integer | no | `500` | Maximum number of returned symbols after line filtering. |
113
+ | `resolveMode` | enum("outline", "exact") | no | `"outline"` | Outline mode returns all symbols (windowed/limited). Exact mode resolves deterministic symbol matches in this file. |
114
+ | `symbolIdExact` | string | no | | Used with resolveMode="exact": exact symbolId match in the target file. |
115
+ | `symbolLabelExact` | string | no | | Used with resolveMode="exact": exact symbol label match in the target file. |
109
116
 
110
117
  ### `read_file`
111
118
 
@@ -117,6 +124,7 @@ Read file content from the local filesystem, with optional 1-based inclusive lin
117
124
  | `start_line` | integer | no | | Optional start line (1-based, inclusive). |
118
125
  | `end_line` | integer | no | | Optional end line (1-based, inclusive). |
119
126
  | `mode` | enum("plain", "annotated") | no | `"plain"` | Output mode. plain returns text only; annotated returns content plus sidecar-backed outline metadata. |
127
+ | `open_symbol` | object | no | | Optional deterministic symbol jump request for this file path. Uses exact symbol resolution within `path` when symbolId/symbolLabel is provided. |
120
128
 
121
129
  ### `list_codebases`
122
130
 
@@ -127,6 +135,15 @@ No parameters.
127
135
 
128
136
  <!-- TOOLS_END -->
129
137
 
138
+ ### `read_file.open_symbol` Fields
139
+
140
+ `open_symbol` resolves symbols inside the same file passed in `read_file.path`.
141
+
142
+ - `symbolId` (string, optional): deterministic symbol id to resolve in `path`.
143
+ - `symbolLabel` (string, optional): exact symbol label to resolve in `path`.
144
+ - `start_line` (integer, optional): direct 1-based start line for span-based jump.
145
+ - `end_line` (integer, optional): direct 1-based end line (inclusive).
146
+
130
147
  ## MCP Config Examples
131
148
 
132
149
  ### JSON-style (Claude Desktop, Cursor)
package/dist/config.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type EmbeddingProvider = 'OpenAI' | 'VoyageAI' | 'Gemini' | 'Ollama';
2
2
  export type VectorStoreProvider = 'Milvus';
3
3
  export type FingerprintSource = 'verified' | 'assumed_v2';
4
+ export declare const DEFAULT_WATCH_DEBOUNCE_MS = 5000;
4
5
  export interface IndexFingerprint {
5
6
  embeddingProvider: EmbeddingProvider;
6
7
  embeddingModel: string;
@@ -37,6 +38,10 @@ export interface CallGraphSidecarInfo {
37
38
  noteCount: number;
38
39
  fingerprint: IndexFingerprint;
39
40
  }
41
+ export interface CodebaseIndexManifest {
42
+ indexedPaths: string[];
43
+ updatedAt: string;
44
+ }
40
45
  export interface CodebaseSnapshotV1 {
41
46
  indexedCodebases: string[];
42
47
  indexingCodebases: string[] | Record<string, number>;
@@ -48,6 +53,9 @@ interface CodebaseInfoBase {
48
53
  fingerprintSource?: FingerprintSource;
49
54
  reindexReason?: 'legacy_unverified_fingerprint' | 'fingerprint_mismatch' | 'missing_fingerprint';
50
55
  callGraphSidecar?: CallGraphSidecarInfo;
56
+ indexManifest?: CodebaseIndexManifest;
57
+ ignoreRulesVersion?: number;
58
+ ignoreControlSignature?: string;
51
59
  }
52
60
  export interface CodebaseInfoIndexing extends CodebaseInfoBase {
53
61
  status: 'indexing';
package/dist/config.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { envManager } from "@zokizuan/satori-core";
2
+ export const DEFAULT_WATCH_DEBOUNCE_MS = 5000;
2
3
  // Helper function to get default model for each provider
3
4
  export function getDefaultModelForProvider(provider) {
4
5
  switch (provider) {
@@ -51,7 +52,6 @@ export function buildRuntimeIndexFingerprint(config, embeddingDimension) {
51
52
  export function createMcpConfig() {
52
53
  const defaultProvider = envManager.get('EMBEDDING_PROVIDER') || 'VoyageAI';
53
54
  const defaultReadFileMaxLines = 1000;
54
- const defaultWatchDebounceMs = 5000;
55
55
  // Parse output dimension from env var
56
56
  const outputDimensionStr = envManager.get('EMBEDDING_OUTPUT_DIMENSION');
57
57
  let encoderOutputDimension;
@@ -92,7 +92,7 @@ export function createMcpConfig() {
92
92
  const watchSyncEnabled = watchSyncEnabledRaw
93
93
  ? watchSyncEnabledRaw.toLowerCase() === 'true'
94
94
  : true;
95
- let watchDebounceMs = defaultWatchDebounceMs;
95
+ let watchDebounceMs = DEFAULT_WATCH_DEBOUNCE_MS;
96
96
  const watchDebounceRaw = envManager.get('MCP_WATCH_DEBOUNCE_MS');
97
97
  if (watchDebounceRaw) {
98
98
  const parsed = Number.parseInt(watchDebounceRaw, 10);
@@ -100,7 +100,7 @@ export function createMcpConfig() {
100
100
  watchDebounceMs = parsed;
101
101
  }
102
102
  else {
103
- console.warn(`[WARN] Invalid MCP_WATCH_DEBOUNCE_MS value: ${watchDebounceRaw}. Using default ${defaultWatchDebounceMs}.`);
103
+ console.warn(`[WARN] Invalid MCP_WATCH_DEBOUNCE_MS value: ${watchDebounceRaw}. Using default ${DEFAULT_WATCH_DEBOUNCE_MS}.`);
104
104
  }
105
105
  }
106
106
  const config = {
@@ -140,7 +140,7 @@ export function logConfigurationSummary(config) {
140
140
  console.log(`[MCP] Embedding Provider: ${config.encoderProvider}`);
141
141
  console.log(`[MCP] Embedding Model: ${config.encoderModel}`);
142
142
  console.log(`[MCP] Milvus Address: ${config.milvusEndpoint || (config.milvusApiToken ? '[Auto-resolve from token]' : '[Not configured]')}`);
143
- console.log(`[MCP] Proactive Watcher: ${config.watchSyncEnabled ? `enabled (${config.watchDebounceMs || 5000}ms debounce)` : 'disabled'}`);
143
+ console.log(`[MCP] Proactive Watcher: ${config.watchSyncEnabled ? `enabled (${config.watchDebounceMs || DEFAULT_WATCH_DEBOUNCE_MS}ms debounce)` : 'disabled'}`);
144
144
  // Log provider-specific configuration without exposing sensitive data
145
145
  switch (config.encoderProvider) {
146
146
  case 'OpenAI':
@@ -34,8 +34,12 @@ export declare class ToolHandlers {
34
34
  private isTestPath;
35
35
  private isDocPath;
36
36
  private isGeneratedPath;
37
+ private isFixturePath;
37
38
  private isEntrypointPath;
38
39
  private classifyPathCategory;
40
+ private classifyNoiseCategory;
41
+ private roundRatio;
42
+ private buildNoiseMitigationHint;
39
43
  private shouldIncludeCategoryInScope;
40
44
  private parseIndexedAtMs;
41
45
  private getStalenessBucket;
@@ -64,6 +68,8 @@ export declare class ToolHandlers {
64
68
  private parseCodebaseFromMetadata;
65
69
  private resolveCollectionCodebasePath;
66
70
  private formatCollectionTimestamp;
71
+ private parseTimestampMs;
72
+ private resolveCollectionSortTimestampMs;
67
73
  private buildZillizCollectionLimitGuidance;
68
74
  private buildCollectionLimitMessage;
69
75
  private clearAllCollectionsForForceReindex;
@@ -4,7 +4,8 @@ import crypto from "node:crypto";
4
4
  import ignore from "ignore";
5
5
  import { COLLECTION_LIMIT_MESSAGE, getSupportedExtensionsForCapability, isLanguageCapabilitySupportedForExtension, isLanguageCapabilitySupportedForLanguage, } from "@zokizuan/satori-core";
6
6
  import { ensureAbsolutePath, truncateContent, trackCodebasePath } from "../utils.js";
7
- import { SEARCH_MAX_CANDIDATES, SEARCH_PROXIMITY_WINDOW, SEARCH_RRF_K, SCOPE_PATH_MULTIPLIERS, STALENESS_THRESHOLDS_MS } from "./search-constants.js";
7
+ import { DEFAULT_WATCH_DEBOUNCE_MS } from "../config.js";
8
+ import { SEARCH_MAX_CANDIDATES, SEARCH_NOISE_HINT_PATTERNS, SEARCH_NOISE_HINT_THRESHOLD, SEARCH_NOISE_HINT_TOP_K, SEARCH_PROXIMITY_WINDOW, SEARCH_RRF_K, SCOPE_PATH_MULTIPLIERS, STALENESS_THRESHOLDS_MS } from "./search-constants.js";
8
9
  import { CallGraphSidecarManager } from "./call-graph.js";
9
10
  const COLLECTION_LIMIT_PATTERNS = [
10
11
  /exceeded the limit number of collections/i,
@@ -15,6 +16,7 @@ const COLLECTION_LIMIT_PATTERNS = [
15
16
  const SATORI_COLLECTION_PREFIXES = ['code_chunks_', 'hybrid_code_chunks_'];
16
17
  const ZILLIZ_FREE_TIER_COLLECTION_LIMIT = 5;
17
18
  const OUTLINE_SUPPORTED_EXTENSIONS = getSupportedExtensionsForCapability('fileOutline');
19
+ const MIN_RELIABLE_COLLECTION_CREATED_AT_MS = Date.UTC(2000, 0, 1);
18
20
  function collectErrorFragments(value, output, visited, depth = 0) {
19
21
  if (value === null || value === undefined || depth > 4 || output.length >= 8) {
20
22
  return;
@@ -404,6 +406,10 @@ export class ToolHandlers {
404
406
  || normalizedPath.endsWith('.min.js')
405
407
  || normalizedPath.endsWith('.min.css');
406
408
  }
409
+ isFixturePath(normalizedPath) {
410
+ return this.hasPathSegment(normalizedPath, 'fixtures')
411
+ || this.hasPathSegment(normalizedPath, '__fixtures__');
412
+ }
407
413
  isEntrypointPath(normalizedPath) {
408
414
  const entryNames = ['main.', 'index.', 'app.', 'server.', 'cli.', 'entry.'];
409
415
  const baseName = normalizedPath.split('/').pop() || '';
@@ -425,6 +431,68 @@ export class ToolHandlers {
425
431
  return 'srcRuntime';
426
432
  return 'neutral';
427
433
  }
434
+ classifyNoiseCategory(relativePath) {
435
+ const normalized = this.normalizeSearchPath(relativePath);
436
+ // Deterministic precedence: generated > tests > fixtures > docs > runtime.
437
+ if (this.isGeneratedPath(normalized)
438
+ || this.hasPathSegment(normalized, 'coverage')
439
+ || this.hasPathSegment(normalized, 'dist')
440
+ || this.hasPathSegment(normalized, 'build'))
441
+ return 'generated';
442
+ if (this.isTestPath(normalized))
443
+ return 'tests';
444
+ if (this.isFixturePath(normalized))
445
+ return 'fixtures';
446
+ if (this.isDocPath(normalized))
447
+ return 'docs';
448
+ return 'runtime';
449
+ }
450
+ roundRatio(value) {
451
+ return Math.round(value * 100) / 100;
452
+ }
453
+ buildNoiseMitigationHint(filesInOrder) {
454
+ if (!Array.isArray(filesInOrder) || filesInOrder.length === 0) {
455
+ return undefined;
456
+ }
457
+ const topK = Math.min(SEARCH_NOISE_HINT_TOP_K, filesInOrder.length);
458
+ if (topK <= 0) {
459
+ return undefined;
460
+ }
461
+ const counts = {
462
+ tests: 0,
463
+ fixtures: 0,
464
+ docs: 0,
465
+ generated: 0,
466
+ runtime: 0,
467
+ };
468
+ for (let i = 0; i < topK; i++) {
469
+ const category = this.classifyNoiseCategory(filesInOrder[i]);
470
+ counts[category] += 1;
471
+ }
472
+ const noisyRatio = (counts.tests + counts.fixtures + counts.docs + counts.generated) / topK;
473
+ if (noisyRatio < SEARCH_NOISE_HINT_THRESHOLD) {
474
+ return undefined;
475
+ }
476
+ const ratios = {
477
+ tests: this.roundRatio(counts.tests / topK),
478
+ fixtures: this.roundRatio(counts.fixtures / topK),
479
+ docs: this.roundRatio(counts.docs / topK),
480
+ generated: this.roundRatio(counts.generated / topK),
481
+ runtime: this.roundRatio(counts.runtime / topK),
482
+ };
483
+ const debounceMs = typeof this.syncManager?.getWatchDebounceMs === 'function'
484
+ ? this.syncManager.getWatchDebounceMs()
485
+ : DEFAULT_WATCH_DEBOUNCE_MS;
486
+ return {
487
+ reason: 'top_results_noise_dominant',
488
+ topK,
489
+ ratios,
490
+ recommendedScope: 'runtime',
491
+ suggestedIgnorePatterns: [...SEARCH_NOISE_HINT_PATTERNS],
492
+ debounceMs,
493
+ nextStep: 'Use scope="runtime". If you still need docs context, use scope="mixed". Edit repo-root .satoriignore using your host/editor, wait one debounce window, rerun search_codebase, or run manage_index with {"action":"sync","path":"<same path used in search_codebase>"} for immediate convergence.',
494
+ };
495
+ }
428
496
  shouldIncludeCategoryInScope(scope, category) {
429
497
  if (scope === 'runtime') {
430
498
  return category !== 'docs' && category !== 'tests';
@@ -574,9 +642,9 @@ export class ToolHandlers {
574
642
  }
575
643
  return 'not_ready';
576
644
  }
577
- getContextIgnorePatterns() {
645
+ getContextIgnorePatterns(codebasePath) {
578
646
  if (typeof this.context.getActiveIgnorePatterns === 'function') {
579
- const patterns = this.context.getActiveIgnorePatterns();
647
+ const patterns = this.context.getActiveIgnorePatterns(codebasePath);
580
648
  if (Array.isArray(patterns)) {
581
649
  return patterns.filter((pattern) => typeof pattern === 'string');
582
650
  }
@@ -585,7 +653,7 @@ export class ToolHandlers {
585
653
  }
586
654
  async rebuildCallGraphForIndex(codebasePath) {
587
655
  try {
588
- const sidecar = await this.callGraphManager.rebuildForCodebase(codebasePath, this.getContextIgnorePatterns());
656
+ const sidecar = await this.callGraphManager.rebuildForCodebase(codebasePath, this.getContextIgnorePatterns(codebasePath));
589
657
  this.snapshotManager.setCodebaseCallGraphSidecar(codebasePath, sidecar);
590
658
  this.snapshotManager.saveCodebaseSnapshot();
591
659
  console.log(`[CALL-GRAPH] Rebuilt sidecar for '${codebasePath}' (${sidecar.nodeCount} nodes, ${sidecar.edgeCount} edges).`);
@@ -596,7 +664,7 @@ export class ToolHandlers {
596
664
  }
597
665
  async rebuildCallGraphForSyncDelta(codebasePath, changedFiles) {
598
666
  try {
599
- const sidecar = await this.callGraphManager.rebuildIfSupportedDelta(codebasePath, changedFiles, this.getContextIgnorePatterns());
667
+ const sidecar = await this.callGraphManager.rebuildIfSupportedDelta(codebasePath, changedFiles, this.getContextIgnorePatterns(codebasePath));
600
668
  if (!sidecar) {
601
669
  return false;
602
670
  }
@@ -707,6 +775,27 @@ export class ToolHandlers {
707
775
  }
708
776
  return new Date(timestamp).toISOString();
709
777
  }
778
+ parseTimestampMs(timestamp) {
779
+ if (!timestamp) {
780
+ return undefined;
781
+ }
782
+ const parsed = Date.parse(timestamp);
783
+ return Number.isFinite(parsed) ? parsed : undefined;
784
+ }
785
+ resolveCollectionSortTimestampMs(createdAt, codebasePath, snapshotLastUpdatedByPath) {
786
+ const createdAtMs = this.parseTimestampMs(createdAt);
787
+ const snapshotMs = codebasePath ? snapshotLastUpdatedByPath.get(codebasePath) : undefined;
788
+ // Prefer collection metadata when it looks reliable.
789
+ if (createdAtMs !== undefined && createdAtMs >= MIN_RELIABLE_COLLECTION_CREATED_AT_MS) {
790
+ return createdAtMs;
791
+ }
792
+ // Fallback to snapshot timestamps when collection metadata is missing or suspicious.
793
+ if (snapshotMs !== undefined) {
794
+ return snapshotMs;
795
+ }
796
+ // Last resort for deterministic ordering when nothing else is available.
797
+ return createdAtMs;
798
+ }
710
799
  async buildZillizCollectionLimitGuidance(targetCodebasePath) {
711
800
  const targetCollectionName = this.context.resolveCollectionName(targetCodebasePath);
712
801
  const vectorDb = this.getVectorStore();
@@ -717,6 +806,13 @@ export class ToolHandlers {
717
806
  for (const codebasePath of trackedCodebases) {
718
807
  byCollectionName.set(this.context.resolveCollectionName(codebasePath), codebasePath);
719
808
  }
809
+ const snapshotLastUpdatedByPath = new Map();
810
+ for (const entry of this.snapshotManager.getAllCodebases()) {
811
+ const lastUpdatedMs = this.parseTimestampMs(entry.info.lastUpdated);
812
+ if (lastUpdatedMs !== undefined) {
813
+ snapshotLastUpdatedByPath.set(entry.path, lastUpdatedMs);
814
+ }
815
+ }
720
816
  const candidates = [];
721
817
  for (const detail of codeCollections) {
722
818
  const codebasePath = await this.resolveCollectionCodebasePath(vectorDb, detail.name, byCollectionName);
@@ -725,15 +821,14 @@ export class ToolHandlers {
725
821
  createdAt: detail.createdAt,
726
822
  codebasePath,
727
823
  isTargetCollection: detail.name === targetCollectionName,
824
+ sortTimestampMs: this.resolveCollectionSortTimestampMs(detail.createdAt, codebasePath, snapshotLastUpdatedByPath),
728
825
  });
729
826
  }
730
827
  candidates.sort((a, b) => {
731
- const aTime = a.createdAt ? Date.parse(a.createdAt) : NaN;
732
- const bTime = b.createdAt ? Date.parse(b.createdAt) : NaN;
733
- const aValid = Number.isFinite(aTime);
734
- const bValid = Number.isFinite(bTime);
828
+ const aValid = Number.isFinite(a.sortTimestampMs);
829
+ const bValid = Number.isFinite(b.sortTimestampMs);
735
830
  if (aValid && bValid) {
736
- return aTime - bTime;
831
+ return a.sortTimestampMs - b.sortTimestampMs;
737
832
  }
738
833
  if (aValid)
739
834
  return -1;
@@ -1237,7 +1332,7 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
1237
1332
  await this.context.loadResolvedIgnorePatterns(absolutePath);
1238
1333
  // Initialize file synchronizer with proper ignore patterns (including project-specific patterns)
1239
1334
  const { FileSynchronizer } = await import("@zokizuan/satori-core");
1240
- const ignorePatterns = this.context.getActiveIgnorePatterns() || [];
1335
+ const ignorePatterns = this.context.getActiveIgnorePatterns(absolutePath) || [];
1241
1336
  console.log(`[BACKGROUND-INDEX] Using ignore patterns: ${ignorePatterns.join(', ')}`);
1242
1337
  const synchronizer = new FileSynchronizer(absolutePath, ignorePatterns);
1243
1338
  await synchronizer.initialize();
@@ -1269,6 +1364,12 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
1269
1364
  console.log(`[BACKGROUND-INDEX] ✅ Indexing completed successfully! Files: ${stats.indexedFiles}, Chunks: ${stats.totalChunks}`);
1270
1365
  // Set codebase to indexed status with complete statistics
1271
1366
  this.snapshotManager.setCodebaseIndexed(absolutePath, stats, this.runtimeFingerprint, 'verified');
1367
+ if (typeof this.context.getTrackedRelativePaths === 'function') {
1368
+ const trackedPaths = this.context.getTrackedRelativePaths(absolutePath);
1369
+ if (typeof this.snapshotManager.setCodebaseIndexManifest === 'function') {
1370
+ this.snapshotManager.setCodebaseIndexManifest(absolutePath, trackedPaths);
1371
+ }
1372
+ }
1272
1373
  this.indexingStats = { indexedFiles: stats.indexedFiles, totalChunks: stats.totalChunks };
1273
1374
  // Save snapshot after updating codebase lists
1274
1375
  this.snapshotManager.saveCodebaseSnapshot();
@@ -1568,6 +1669,7 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
1568
1669
  }
1569
1670
  } : {})
1570
1671
  }));
1672
+ const noiseMitigationHint = this.buildNoiseMitigationHint(rawResults.map((result) => result.file));
1571
1673
  const envelope = {
1572
1674
  status: "ok",
1573
1675
  path: absolutePath,
@@ -1578,6 +1680,7 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
1578
1680
  resultMode: "raw",
1579
1681
  freshnessDecision,
1580
1682
  ...(searchWarnings.length > 0 ? { warnings: searchWarnings } : {}),
1683
+ ...(noiseMitigationHint ? { hints: { version: 1, noiseMitigation: noiseMitigationHint } } : {}),
1581
1684
  results: rawResults
1582
1685
  };
1583
1686
  return {
@@ -1666,6 +1769,8 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
1666
1769
  return labelCmp;
1667
1770
  return this.compareNullableStringsAsc(a.symbolId, b.symbolId);
1668
1771
  });
1772
+ const visibleGroupedResults = groupedResults.slice(0, input.limit);
1773
+ const noiseMitigationHint = this.buildNoiseMitigationHint(visibleGroupedResults.map((result) => result.file));
1669
1774
  const envelope = {
1670
1775
  status: "ok",
1671
1776
  path: absolutePath,
@@ -1676,7 +1781,8 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
1676
1781
  resultMode: "grouped",
1677
1782
  freshnessDecision,
1678
1783
  ...(searchWarnings.length > 0 ? { warnings: searchWarnings } : {}),
1679
- results: groupedResults.slice(0, input.limit)
1784
+ ...(noiseMitigationHint ? { hints: { version: 1, noiseMitigation: noiseMitigationHint } } : {}),
1785
+ results: visibleGroupedResults
1680
1786
  };
1681
1787
  return {
1682
1788
  content: [{ type: "text", text: JSON.stringify(envelope, null, 2) }],
@@ -1705,6 +1811,9 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
1705
1811
  : 500;
1706
1812
  const requestedStartLine = Number.isFinite(args?.start_line) ? Math.max(1, Number(args.start_line)) : undefined;
1707
1813
  const requestedEndLine = Number.isFinite(args?.end_line) ? Math.max(1, Number(args.end_line)) : undefined;
1814
+ const resolveMode = args?.resolveMode === 'exact' ? 'exact' : 'outline';
1815
+ const symbolIdExact = typeof args?.symbolIdExact === 'string' ? args.symbolIdExact.trim() : undefined;
1816
+ const symbolLabelExact = typeof args?.symbolLabelExact === 'string' ? args.symbolLabelExact.trim() : undefined;
1708
1817
  try {
1709
1818
  await this.syncIndexedCodebasesFromCloud();
1710
1819
  const absoluteRoot = ensureAbsolutePath(args.path);
@@ -1859,13 +1968,55 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
1859
1968
  }
1860
1969
  };
1861
1970
  }));
1862
- const hasMore = symbols.length > limitSymbols;
1863
1971
  const missingSymbolMetadataCount = sidecar.notes.filter((note) => {
1864
1972
  return note.type === 'missing_symbol_metadata' && this.normalizeRelativeFilePath(note.file) === normalizedFile;
1865
1973
  }).length;
1866
1974
  const warnings = missingSymbolMetadataCount > 0
1867
1975
  ? [`OUTLINE_MISSING_SYMBOL_METADATA:${missingSymbolMetadataCount}`]
1868
1976
  : undefined;
1977
+ if (resolveMode === 'exact') {
1978
+ const exactMatches = this.sortFileOutlineSymbols(symbols.filter((symbol) => {
1979
+ if (symbolIdExact && symbol.symbolId !== symbolIdExact) {
1980
+ return false;
1981
+ }
1982
+ if (symbolLabelExact && symbol.symbolLabel !== symbolLabelExact) {
1983
+ return false;
1984
+ }
1985
+ return true;
1986
+ }));
1987
+ if (exactMatches.length === 0) {
1988
+ const payload = {
1989
+ status: 'not_found',
1990
+ path: absoluteRoot,
1991
+ file: normalizedFile,
1992
+ outline: null,
1993
+ hasMore: false,
1994
+ message: 'No exact symbol match found in file outline.',
1995
+ ...(warnings ? { warnings } : {})
1996
+ };
1997
+ return {
1998
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
1999
+ };
2000
+ }
2001
+ const hasMoreExact = exactMatches.length > limitSymbols;
2002
+ const exactPayload = {
2003
+ status: exactMatches.length > 1 ? 'ambiguous' : 'ok',
2004
+ path: absoluteRoot,
2005
+ file: normalizedFile,
2006
+ outline: {
2007
+ symbols: exactMatches.slice(0, limitSymbols)
2008
+ },
2009
+ hasMore: hasMoreExact,
2010
+ ...(exactMatches.length > 1 ? {
2011
+ message: `Multiple exact symbol matches found (${exactMatches.length}). Narrow with symbolIdExact for deterministic selection.`
2012
+ } : {}),
2013
+ ...(warnings ? { warnings } : {})
2014
+ };
2015
+ return {
2016
+ content: [{ type: "text", text: JSON.stringify(exactPayload, null, 2) }]
2017
+ };
2018
+ }
2019
+ const hasMore = symbols.length > limitSymbols;
1869
2020
  const payload = {
1870
2021
  status: 'ok',
1871
2022
  path: absoluteRoot,
@@ -2329,6 +2480,12 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
2329
2480
  removed: syncStats.removed,
2330
2481
  modified: syncStats.modified
2331
2482
  };
2483
+ if (typeof this.context.getTrackedRelativePaths === 'function') {
2484
+ const trackedPaths = this.context.getTrackedRelativePaths(absolutePath);
2485
+ if (typeof this.snapshotManager.setCodebaseIndexManifest === 'function') {
2486
+ this.snapshotManager.setCodebaseIndexManifest(absolutePath, trackedPaths);
2487
+ }
2488
+ }
2332
2489
  // Store sync result in snapshot
2333
2490
  this.snapshotManager.setCodebaseSyncCompleted(absolutePath, syncTotals, this.runtimeFingerprint, 'verified');
2334
2491
  this.snapshotManager.saveCodebaseSnapshot();
@@ -1,6 +1,9 @@
1
1
  export declare const SEARCH_RRF_K = 60;
2
2
  export declare const SEARCH_MAX_CANDIDATES = 80;
3
3
  export declare const SEARCH_PROXIMITY_WINDOW = 25;
4
+ export declare const SEARCH_NOISE_HINT_TOP_K = 5;
5
+ export declare const SEARCH_NOISE_HINT_THRESHOLD = 0.6;
6
+ export declare const SEARCH_NOISE_HINT_PATTERNS: readonly ["**/*.test.*", "**/*.spec.*", "**/__tests__/**", "**/__fixtures__/**", "**/fixtures/**", "coverage/**"];
4
7
  export declare const STALENESS_THRESHOLDS_MS: {
5
8
  readonly fresh: number;
6
9
  readonly aging: number;
@@ -8,6 +11,7 @@ export declare const STALENESS_THRESHOLDS_MS: {
8
11
  export type SearchScope = 'runtime' | 'mixed' | 'docs';
9
12
  export type SearchResultMode = 'grouped' | 'raw';
10
13
  export type SearchGroupBy = 'symbol' | 'file';
14
+ export type SearchNoiseCategory = 'tests' | 'fixtures' | 'docs' | 'generated' | 'runtime';
11
15
  export type PathCategory = 'entrypoint' | 'core' | 'srcRuntime' | 'neutral' | 'tests' | 'docs' | 'generated';
12
16
  export declare const SCOPE_PATH_MULTIPLIERS: Record<SearchScope, Record<PathCategory, number>>;
13
17
  //# sourceMappingURL=search-constants.d.ts.map
@@ -1,6 +1,16 @@
1
1
  export const SEARCH_RRF_K = 60;
2
2
  export const SEARCH_MAX_CANDIDATES = 80;
3
3
  export const SEARCH_PROXIMITY_WINDOW = 25;
4
+ export const SEARCH_NOISE_HINT_TOP_K = 5;
5
+ export const SEARCH_NOISE_HINT_THRESHOLD = 0.60;
6
+ export const SEARCH_NOISE_HINT_PATTERNS = [
7
+ '**/*.test.*',
8
+ '**/*.spec.*',
9
+ '**/__tests__/**',
10
+ '**/__fixtures__/**',
11
+ '**/fixtures/**',
12
+ 'coverage/**',
13
+ ];
4
14
  export const STALENESS_THRESHOLDS_MS = {
5
15
  fresh: 30 * 60 * 1000,
6
16
  aging: 24 * 60 * 60 * 1000,
@@ -1,5 +1,5 @@
1
1
  import { FreshnessDecision } from "./sync.js";
2
- import { SearchGroupBy, SearchResultMode, SearchScope } from "./search-constants.js";
2
+ import { SearchGroupBy, SearchNoiseCategory, SearchResultMode, SearchScope } from "./search-constants.js";
3
3
  import { FingerprintSource, IndexFingerprint } from "../config.js";
4
4
  export type StalenessBucket = "fresh" | "aging" | "stale" | "unknown";
5
5
  export interface SearchSpan {
@@ -65,6 +65,19 @@ export interface FingerprintCompatibilityDiagnostics {
65
65
  reindexReason?: "legacy_unverified_fingerprint" | "fingerprint_mismatch" | "missing_fingerprint";
66
66
  statusAtCheck?: "indexed" | "indexing" | "indexfailed" | "sync_completed" | "requires_reindex" | "not_found";
67
67
  }
68
+ export interface SearchNoiseMitigationHint {
69
+ reason: "top_results_noise_dominant";
70
+ topK: number;
71
+ ratios: Record<SearchNoiseCategory, number>;
72
+ recommendedScope: "runtime";
73
+ suggestedIgnorePatterns: string[];
74
+ debounceMs: number;
75
+ nextStep: string;
76
+ }
77
+ export interface SearchResponseHints extends Record<string, unknown> {
78
+ version?: 1;
79
+ noiseMitigation?: SearchNoiseMitigationHint;
80
+ }
68
81
  interface SearchBaseResponseEnvelope {
69
82
  status: "ok" | "requires_reindex" | "not_indexed";
70
83
  path: string;
@@ -77,7 +90,7 @@ interface SearchBaseResponseEnvelope {
77
90
  } | null;
78
91
  warnings?: string[];
79
92
  message?: string;
80
- hints?: Record<string, unknown>;
93
+ hints?: SearchResponseHints;
81
94
  compatibility?: FingerprintCompatibilityDiagnostics;
82
95
  }
83
96
  export interface SearchGroupedResponseEnvelope extends SearchBaseResponseEnvelope {
@@ -104,8 +117,11 @@ export interface FileOutlineInput {
104
117
  start_line?: number;
105
118
  end_line?: number;
106
119
  limitSymbols?: number;
120
+ resolveMode?: "outline" | "exact";
121
+ symbolIdExact?: string;
122
+ symbolLabelExact?: string;
107
123
  }
108
- export type FileOutlineStatus = "ok" | "not_found" | "requires_reindex" | "unsupported";
124
+ export type FileOutlineStatus = "ok" | "not_found" | "requires_reindex" | "unsupported" | "ambiguous";
109
125
  export interface FileOutlineSymbolResult {
110
126
  symbolId: string;
111
127
  symbolLabel: string;
@@ -49,6 +49,11 @@ export declare class SnapshotManager {
49
49
  }, indexFingerprint?: IndexFingerprint, fingerprintSource?: FingerprintSource): void;
50
50
  setCodebaseRequiresReindex(codebasePath: string, reason: 'legacy_unverified_fingerprint' | 'fingerprint_mismatch' | 'missing_fingerprint', message?: string): void;
51
51
  setCodebaseCallGraphSidecar(codebasePath: string, sidecar: CallGraphSidecarInfo): void;
52
+ setCodebaseIndexManifest(codebasePath: string, indexedPaths: string[]): void;
53
+ getCodebaseIndexedPaths(codebasePath: string): string[];
54
+ setCodebaseIgnoreRulesVersion(codebasePath: string, version: number): void;
55
+ getCodebaseIgnoreControlSignature(codebasePath: string): string | undefined;
56
+ setCodebaseIgnoreControlSignature(codebasePath: string, signature: string): void;
52
57
  getCodebaseCallGraphSidecar(codebasePath: string): CallGraphSidecarInfo | undefined;
53
58
  removeCodebaseCompletely(codebasePath: string): void;
54
59
  removeIndexedCodebase(codebasePath: string): void;