| 1 | using System.Collections.Frozen; |
| 2 | using System.Reflection; |
| 3 | using System.Text.Json; |
| 4 | using System.Text.Json.Serialization; |
| 5 | using HybridCodebaseIndex.Core; |
| 6 | |
| 7 | namespace HybridCodebaseIndex.Mcp; |
| 8 | |
| 9 | internal static class ToolHandlers |
| 10 | { |
| 11 | private static readonly CodebaseIndexService Service = new(); |
| 12 | private static readonly IndexWatchManager Watchers = new(Service); |
| 13 | |
| 14 | private static readonly JsonSerializerOptions JsonOut = new() |
| 15 | { |
| 16 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, |
| 17 | WriteIndented = false, |
| 18 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, |
| 19 | }; |
| 20 | |
| 21 | private static readonly JsonSerializerOptions JsonOutWithNulls = new() |
| 22 | { |
| 23 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, |
| 24 | WriteIndented = false, |
| 25 | DefaultIgnoreCondition = JsonIgnoreCondition.Never, |
| 26 | }; |
| 27 | |
| 28 | internal static string Handle(string name, IReadOnlyDictionary<string, JsonElement> args) |
| 29 | { |
| 30 | args ??= FrozenDictionary<string, JsonElement>.Empty; |
| 31 | return name switch |
| 32 | { |
| 33 | "codebase_index_version" => HandleVersion(), |
| 34 | "codebase_index_search" => HandleSearch(args), |
| 35 | "codebase_index_explain" => HandleExplain(args), |
| 36 | "codebase_index_status" => HandleStatus(args), |
| 37 | "codebase_index_reindex" => HandleReindex(args), |
| 38 | "codebase_index_watch" => HandleWatch(args), |
| 39 | "codebase_index_verify" => HandleVerify(args), |
| 40 | "codebase_index_draft_doc" => HandleDraftDoc(args), |
| 41 | "codebase_index_vec_reindex" => HandleVecReindex(args), |
| 42 | _ => throw new ArgumentException($"Unknown tool: {name}", nameof(name)), |
| 43 | }; |
| 44 | } |
| 45 | |
| 46 | private static string HandleVersion() |
| 47 | { |
| 48 | var asm = typeof(ToolHandlers).Assembly; |
| 49 | var name = asm.GetName(); |
| 50 | var info = asm.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion; |
| 51 | |
| 52 | var dto = new VersionResultDto( |
| 53 | AssemblyName: name.Name ?? "unknown", |
| 54 | AssemblyVersion: name.Version?.ToString(), |
| 55 | InformationalVersion: info, |
| 56 | FrameworkDescription: System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription, |
| 57 | RuntimeIdentifier: System.Runtime.InteropServices.RuntimeInformation.RuntimeIdentifier, |
| 58 | OsDescription: System.Runtime.InteropServices.RuntimeInformation.OSDescription, |
| 59 | ProcessArchitecture: System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString()); |
| 60 | |
| 61 | return JsonSerializer.Serialize(dto, JsonOut); |
| 62 | } |
| 63 | |
| 64 | private static string HandleSearch(IReadOnlyDictionary<string, JsonElement> args) |
| 65 | { |
| 66 | var ws = RequireString(args, "workspace_path"); |
| 67 | var sln = OptionalString(args, "solution_path"); |
| 68 | var q = RequireString(args, "query"); |
| 69 | var topN = OptionalInt(args, "top_n") ?? 15; |
| 70 | if (topN < 1) |
| 71 | topN = 1; |
| 72 | if (topN > 128) |
| 73 | topN = 128; |
| 74 | |
| 75 | var pathPrefix = OptionalString(args, "path_prefix"); |
| 76 | var excludePrefixes = OptionalStringArray(args, "exclude_path_prefixes"); |
| 77 | var extensions = OptionalStringArray(args, "extensions"); |
| 78 | |
| 79 | var semantic = OptionalBool(args, "semantic") ?? false; |
| 80 | var alpha = OptionalDouble(args, "alpha") ?? 0.65; |
| 81 | var beta = OptionalDouble(args, "beta") ?? 0.35; |
| 82 | var vecTopK = OptionalInt(args, "vec_top_k") ?? 30; |
| 83 | |
| 84 | var (response, err) = Service.SearchHybridAsync(ws, sln, q, topN, pathPrefix, excludePrefixes, extensions, semantic, alpha, beta, vecTopK).GetAwaiter().GetResult(); |
| 85 | var dto = new SearchResultDto( |
| 86 | Err: err, |
| 87 | IndexFormatVersion: response.IndexFormatVersion, |
| 88 | Query: response.Query, |
| 89 | DatabasePath: response.DatabasePath, |
| 90 | Hits: response.Hits.Select(static h => new HitDto(h.HitId, h.Path, h.Extension, h.HitKind, h.RankScore, h.FtsScore, h.VecScore, h.Snippet, h.LineStart, h.LineEnd, h.ChunkCharCount, h.LastWriteUtcIso)).ToList()); |
| 91 | |
| 92 | return JsonSerializer.Serialize(dto, JsonOut); |
| 93 | } |
| 94 | |
| 95 | private static string HandleExplain(IReadOnlyDictionary<string, JsonElement> args) |
| 96 | { |
| 97 | var ws = RequireString(args, "workspace_path"); |
| 98 | var sln = OptionalString(args, "solution_path"); |
| 99 | var hitId = RequireLong(args, "hit_id"); |
| 100 | |
| 101 | var resp = Service.ExplainHitAsync(ws, sln, hitId).GetAwaiter().GetResult(); |
| 102 | var dto = new ExplainResultDto( |
| 103 | Err: resp.Err, |
| 104 | IndexFormatVersion: resp.IndexFormatVersion, |
| 105 | DatabasePath: resp.DatabasePath, |
| 106 | Hit: resp.Hit is null |
| 107 | ? null |
| 108 | : new HitDto(resp.Hit.HitId, resp.Hit.Path, resp.Hit.Extension, resp.Hit.HitKind, resp.Hit.RankScore, resp.Hit.FtsScore, resp.Hit.VecScore, resp.Hit.Snippet, resp.Hit.LineStart, resp.Hit.LineEnd, resp.Hit.ChunkCharCount, resp.Hit.LastWriteUtcIso)); |
| 109 | |
| 110 | return JsonSerializer.Serialize(dto, JsonOut); |
| 111 | } |
| 112 | |
| 113 | private static string HandleStatus(IReadOnlyDictionary<string, JsonElement> args) |
| 114 | { |
| 115 | var ws = RequireString(args, "workspace_path"); |
| 116 | var sln = OptionalString(args, "solution_path"); |
| 117 | var st = Service.GetStatusAsync(ws, sln).GetAwaiter().GetResult(); |
| 118 | var dto = new StatusResultDto( |
| 119 | IndexFormatVersion: st.IndexFormatVersion, |
| 120 | DatabasePath: st.DatabasePath, |
| 121 | DatabaseExists: st.DatabaseExists, |
| 122 | DocumentCount: st.DocumentCount, |
| 123 | DocumentCountMayBeStale: st.DocumentCountMayBeStale, |
| 124 | IndexedAtIso: st.IndexedAtIso, |
| 125 | WorkspaceRoot: st.WorkspaceRootNormalized, |
| 126 | LastReindexError: st.LastReindexError, |
| 127 | LastReindexErrorAtIso: st.LastReindexErrorAtIso, |
| 128 | SettingsSource: st.SettingsSource, |
| 129 | SettingsParseError: st.SettingsParseError, |
| 130 | EffectiveSettings: st.EffectiveSettings is null |
| 131 | ? null |
| 132 | : new EffectiveSettingsDto( |
| 133 | st.EffectiveSettings.IncludeCsInFts, |
| 134 | st.EffectiveSettings.ExtraIncludeRoots.ToList(), |
| 135 | st.EffectiveSettings.ExcludeRoots.ToList(), |
| 136 | st.EffectiveSettings.EffectiveExtensions.ToList(), |
| 137 | st.EffectiveSettings.ExcludePathSegments.ToList(), |
| 138 | st.EffectiveSettings.IgnoreFiles.ToList(), |
| 139 | st.EffectiveSettings.MaxIndexedFileBytes, |
| 140 | st.EffectiveSettings.ChunkLines, |
| 141 | st.EffectiveSettings.ChunkOverlapLines, |
| 142 | st.EffectiveSettings.BinaryProbeBytes), |
| 143 | ReindexState: st.ReindexState, |
| 144 | ReindexStartedAtIso: st.ReindexStartedAtIso); |
| 145 | |
| 146 | // Keep the contract stable: include nullable fields explicitly. |
| 147 | return JsonSerializer.Serialize(dto, JsonOutWithNulls); |
| 148 | } |
| 149 | |
| 150 | private static string HandleReindex(IReadOnlyDictionary<string, JsonElement> args) |
| 151 | { |
| 152 | var ws = RequireString(args, "workspace_path"); |
| 153 | var sln = OptionalString(args, "solution_path"); |
| 154 | var full = OptionalBool(args, "full_rebuild") ?? false; |
| 155 | var summary = full |
| 156 | ? Service.FullRebuildAsync(ws, sln).GetAwaiter().GetResult() |
| 157 | : Service.FullReindexAsync(ws, sln).GetAwaiter().GetResult(); |
| 158 | var dto = new ReindexResultDto( |
| 159 | IndexFormatVersion: summary.IndexFormatVersion, |
| 160 | DatabasePath: summary.DatabasePath, |
| 161 | FilesIndexed: summary.FilesIndexed, |
| 162 | FilesSkippedTooLarge: summary.FilesSkippedTooLarge, |
| 163 | FilesSkippedBinary: summary.FilesSkippedBinary, |
| 164 | FilesSkippedExcluded: summary.FilesSkippedExcluded, |
| 165 | SkippedReasonCounts: summary.SkippedReasonCounts, |
| 166 | SkippedTopPathPrefixes: summary.SkippedTopPathPrefixes.Select(static p => new TopPrefixDto(p.PathPrefix, p.Count)).ToList(), |
| 167 | SkippedSample: summary.SkippedSample.Select(static s => new SkippedDto(s.Path, s.Reason)).ToList(), |
| 168 | DurationMs: (long)summary.Duration.TotalMilliseconds); |
| 169 | |
| 170 | return JsonSerializer.Serialize(dto, JsonOut); |
| 171 | } |
| 172 | |
| 173 | private static string HandleWatch(IReadOnlyDictionary<string, JsonElement> args) |
| 174 | { |
| 175 | var ws = RequireString(args, "workspace_path"); |
| 176 | var sln = OptionalString(args, "solution_path"); |
| 177 | var enabled = OptionalBool(args, "enabled") ?? false; |
| 178 | var debounceMs = OptionalInt(args, "debounce_ms") ?? 750; |
| 179 | if (debounceMs < 50) |
| 180 | debounceMs = 50; |
| 181 | if (debounceMs > 60_000) |
| 182 | debounceMs = 60_000; |
| 183 | |
| 184 | Watchers.SetEnabled(ws, sln, enabled, debounceMs); |
| 185 | |
| 186 | return JsonSerializer.Serialize(new |
| 187 | { |
| 188 | workspacePath = ws, |
| 189 | solutionPath = sln, |
| 190 | enabled, |
| 191 | debounceMs, |
| 192 | }, JsonOut); |
| 193 | } |
| 194 | |
| 195 | private static string HandleVerify(IReadOnlyDictionary<string, JsonElement> args) |
| 196 | { |
| 197 | var ws = RequireString(args, "workspace_path"); |
| 198 | var sln = OptionalString(args, "solution_path"); |
| 199 | var ids = RequireStringArray(args, "identifiers"); |
| 200 | |
| 201 | var topN = OptionalInt(args, "top_n") ?? 5; |
| 202 | if (topN < 1) topN = 1; |
| 203 | if (topN > 25) topN = 25; |
| 204 | |
| 205 | var suggestions = OptionalInt(args, "suggestions") ?? 8; |
| 206 | if (suggestions < 0) suggestions = 0; |
| 207 | if (suggestions > 25) suggestions = 25; |
| 208 | |
| 209 | var pathPrefix = OptionalString(args, "path_prefix"); |
| 210 | var excludePrefixes = OptionalStringArray(args, "exclude_path_prefixes"); |
| 211 | var extensions = OptionalStringArray(args, "extensions") ?? [".cs"]; |
| 212 | |
| 213 | static string NormalizeId(string s) |
| 214 | { |
| 215 | // Best-effort token for FTS: strip obvious punctuation so prefix search works better. |
| 216 | // Keep dots/underscores because they're meaningful in namespaces/member access for humans. |
| 217 | return s.Trim(); |
| 218 | } |
| 219 | |
| 220 | static string PrefixToken(string id) |
| 221 | { |
| 222 | // Take the last segment (Foo.Bar.Baz -> Baz) for suggestions. |
| 223 | var lastDot = id.LastIndexOf('.'); |
| 224 | var token = lastDot >= 0 ? id[(lastDot + 1)..] : id; |
| 225 | token = token.Trim(); |
| 226 | if (token.Length > 24) |
| 227 | token = token[..24]; |
| 228 | return token; |
| 229 | } |
| 230 | |
| 231 | var results = new List<VerifyItemDto>(capacity: ids.Count); |
| 232 | |
| 233 | foreach (var raw in ids) |
| 234 | { |
| 235 | var id = NormalizeId(raw); |
| 236 | if (string.IsNullOrWhiteSpace(id)) |
| 237 | continue; |
| 238 | |
| 239 | var (resp, err) = Service.SearchAsync(ws, sln, id, topN, pathPrefix, excludePrefixes, extensions).GetAwaiter().GetResult(); |
| 240 | if (!string.IsNullOrEmpty(err)) |
| 241 | { |
| 242 | results.Add(new VerifyItemDto(id, Exists: false, MatchKind: "strict", Segment: null, Err: err, Hits: [], Suggestions: [])); |
| 243 | continue; |
| 244 | } |
| 245 | |
| 246 | var hits = resp.Hits |
| 247 | .Take(topN) |
| 248 | .Select(static h => new HitDto(h.HitId, h.Path, h.Extension, h.HitKind, h.RankScore, h.FtsScore, h.VecScore, h.Snippet, h.LineStart, h.LineEnd, h.ChunkCharCount, h.LastWriteUtcIso)) |
| 249 | .ToList(); |
| 250 | |
| 251 | if (hits.Count > 0) |
| 252 | { |
| 253 | results.Add(new VerifyItemDto(id, Exists: true, MatchKind: "strict", Segment: null, Err: null, Hits: hits, Suggestions: [])); |
| 254 | continue; |
| 255 | } |
| 256 | |
| 257 | var seg = PrefixToken(id); |
| 258 | if (seg.Length > 0 && !string.Equals(seg, id, StringComparison.Ordinal)) |
| 259 | { |
| 260 | var (segResp, segErr) = Service.SearchAsync(ws, sln, seg, topN, pathPrefix, excludePrefixes, extensions).GetAwaiter().GetResult(); |
| 261 | if (string.IsNullOrEmpty(segErr) && segResp.Hits.Count > 0) |
| 262 | { |
| 263 | var segHits = segResp.Hits |
| 264 | .Take(topN) |
| 265 | .Select(static h => new HitDto(h.HitId, h.Path, h.Extension, h.HitKind, h.RankScore, h.FtsScore, h.VecScore, h.Snippet, h.LineStart, h.LineEnd, h.ChunkCharCount, h.LastWriteUtcIso)) |
| 266 | .ToList(); |
| 267 | results.Add(new VerifyItemDto(id, Exists: true, MatchKind: "segment", Segment: seg, Err: null, Hits: segHits, Suggestions: [])); |
| 268 | continue; |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | if (suggestions == 0) |
| 273 | { |
| 274 | results.Add(new VerifyItemDto(id, Exists: false, MatchKind: "strict", Segment: null, Err: null, Hits: [], Suggestions: [])); |
| 275 | continue; |
| 276 | } |
| 277 | |
| 278 | var token = PrefixToken(id); |
| 279 | var q = token.Length == 0 ? id : $"{token}*"; |
| 280 | var (sugResp, sugErr) = Service.SearchAsync(ws, sln, q, Math.Max(suggestions, 5), pathPrefix, excludePrefixes, extensions).GetAwaiter().GetResult(); |
| 281 | if (!string.IsNullOrEmpty(sugErr)) |
| 282 | { |
| 283 | results.Add(new VerifyItemDto(id, Exists: false, MatchKind: "strict", Segment: null, Err: null, Hits: [], Suggestions: [])); |
| 284 | continue; |
| 285 | } |
| 286 | |
| 287 | var sug = sugResp.Hits |
| 288 | .Take(suggestions) |
| 289 | .Select(static h => new HitDto(h.HitId, h.Path, h.Extension, h.HitKind, h.RankScore, h.FtsScore, h.VecScore, h.Snippet, h.LineStart, h.LineEnd, h.ChunkCharCount, h.LastWriteUtcIso)) |
| 290 | .ToList(); |
| 291 | |
| 292 | results.Add(new VerifyItemDto(id, Exists: false, MatchKind: "strict", Segment: null, Err: null, Hits: [], Suggestions: sug)); |
| 293 | } |
| 294 | |
| 295 | return JsonSerializer.Serialize(new VerifyResultDto( |
| 296 | WorkspacePath: ws, |
| 297 | SolutionPath: sln, |
| 298 | Extensions: extensions, |
| 299 | PathPrefix: pathPrefix, |
| 300 | ExcludePathPrefixes: excludePrefixes, |
| 301 | TopN: topN, |
| 302 | Suggestions: suggestions, |
| 303 | Items: results), JsonOut); |
| 304 | } |
| 305 | |
| 306 | private static string HandleDraftDoc(IReadOnlyDictionary<string, JsonElement> args) |
| 307 | { |
| 308 | var ws = RequireString(args, "workspace_path"); |
| 309 | var sln = OptionalString(args, "solution_path"); |
| 310 | var title = RequireString(args, "title"); |
| 311 | var paths = RequireStringArray(args, "changed_paths"); |
| 312 | |
| 313 | var resp = Service.DraftDocAsync(ws, sln, title, paths).GetAwaiter().GetResult(); |
| 314 | return JsonSerializer.Serialize(new DraftDocDto( |
| 315 | Err: resp.Err, |
| 316 | IndexFormatVersion: resp.IndexFormatVersion, |
| 317 | DatabasePath: resp.DatabasePath, |
| 318 | Title: resp.Title, |
| 319 | Markdown: resp.Markdown), JsonOut); |
| 320 | } |
| 321 | |
| 322 | private static string RequireString(IReadOnlyDictionary<string, JsonElement> args, string key) |
| 323 | { |
| 324 | if (!args.TryGetValue(key, out var el) || el.ValueKind != JsonValueKind.String) |
| 325 | throw new ArgumentException($"Missing or invalid '{key}' (string required)."); |
| 326 | |
| 327 | var s = el.GetString(); |
| 328 | if (string.IsNullOrWhiteSpace(s)) |
| 329 | throw new ArgumentException($"Missing or invalid '{key}' (string required)."); |
| 330 | |
| 331 | return s.Trim(); |
| 332 | } |
| 333 | |
| 334 | private static string? OptionalString(IReadOnlyDictionary<string, JsonElement> args, string key) |
| 335 | { |
| 336 | if (!args.TryGetValue(key, out var el) || el.ValueKind != JsonValueKind.String) |
| 337 | return null; |
| 338 | var s = el.GetString(); |
| 339 | return string.IsNullOrWhiteSpace(s) ? null : s.Trim(); |
| 340 | } |
| 341 | |
| 342 | private static int? OptionalInt(IReadOnlyDictionary<string, JsonElement> args, string key) |
| 343 | { |
| 344 | if (!args.TryGetValue(key, out var el)) |
| 345 | return null; |
| 346 | return el.TryGetInt32(out var i) ? i : null; |
| 347 | } |
| 348 | |
| 349 | private static List<string>? OptionalStringArray(IReadOnlyDictionary<string, JsonElement> args, string key) |
| 350 | { |
| 351 | if (!args.TryGetValue(key, out var el) || el.ValueKind != JsonValueKind.Array) |
| 352 | return null; |
| 353 | |
| 354 | var list = new List<string>(); |
| 355 | foreach (var it in el.EnumerateArray()) |
| 356 | { |
| 357 | if (it.ValueKind != JsonValueKind.String) |
| 358 | continue; |
| 359 | var s = it.GetString(); |
| 360 | if (!string.IsNullOrWhiteSpace(s)) |
| 361 | list.Add(s.Trim()); |
| 362 | } |
| 363 | return list.Count == 0 ? null : list; |
| 364 | } |
| 365 | |
| 366 | private static List<string> RequireStringArray(IReadOnlyDictionary<string, JsonElement> args, string key) |
| 367 | { |
| 368 | if (!args.TryGetValue(key, out var el) || el.ValueKind != JsonValueKind.Array) |
| 369 | throw new ArgumentException($"Missing or invalid '{key}' (string[] required)."); |
| 370 | |
| 371 | var list = new List<string>(); |
| 372 | foreach (var it in el.EnumerateArray()) |
| 373 | { |
| 374 | if (it.ValueKind != JsonValueKind.String) |
| 375 | continue; |
| 376 | var s = it.GetString(); |
| 377 | if (!string.IsNullOrWhiteSpace(s)) |
| 378 | list.Add(s.Trim()); |
| 379 | } |
| 380 | |
| 381 | if (list.Count == 0) |
| 382 | throw new ArgumentException($"Missing or invalid '{key}' (string[] required)."); |
| 383 | |
| 384 | return list; |
| 385 | } |
| 386 | |
| 387 | private static bool? OptionalBool(IReadOnlyDictionary<string, JsonElement> args, string key) |
| 388 | { |
| 389 | if (!args.TryGetValue(key, out var el)) |
| 390 | return null; |
| 391 | return el.ValueKind == JsonValueKind.True ? true |
| 392 | : el.ValueKind == JsonValueKind.False ? false |
| 393 | : null; |
| 394 | } |
| 395 | |
| 396 | private static long RequireLong(IReadOnlyDictionary<string, JsonElement> args, string key) |
| 397 | { |
| 398 | if (!args.TryGetValue(key, out var el)) |
| 399 | throw new ArgumentException($"Missing or invalid '{key}' (integer required)."); |
| 400 | |
| 401 | if (el.ValueKind == JsonValueKind.Number && el.TryGetInt64(out var v)) |
| 402 | return v; |
| 403 | |
| 404 | throw new ArgumentException($"Missing or invalid '{key}' (integer required)."); |
| 405 | } |
| 406 | |
| 407 | private sealed record SearchResultDto( |
| 408 | string? Err, |
| 409 | int IndexFormatVersion, |
| 410 | string Query, |
| 411 | string DatabasePath, |
| 412 | List<HitDto> Hits); |
| 413 | |
| 414 | private sealed record HitDto( |
| 415 | long HitId, |
| 416 | string Path, |
| 417 | string Extension, |
| 418 | string HitKind, |
| 419 | double RankScore, |
| 420 | double? FtsScore, |
| 421 | double? VecScore, |
| 422 | string? Snippet, |
| 423 | int LineStart, |
| 424 | int LineEnd, |
| 425 | int ChunkCharCount, |
| 426 | string? LastWriteUtcIso); |
| 427 | |
| 428 | private sealed record ExplainResultDto( |
| 429 | string? Err, |
| 430 | int IndexFormatVersion, |
| 431 | string DatabasePath, |
| 432 | HitDto? Hit); |
| 433 | |
| 434 | private sealed record StatusResultDto( |
| 435 | int IndexFormatVersion, |
| 436 | string DatabasePath, |
| 437 | bool DatabaseExists, |
| 438 | int DocumentCount, |
| 439 | bool DocumentCountMayBeStale, |
| 440 | string? IndexedAtIso, |
| 441 | string? WorkspaceRoot, |
| 442 | string? LastReindexError, |
| 443 | string? LastReindexErrorAtIso, |
| 444 | string SettingsSource, |
| 445 | string? SettingsParseError, |
| 446 | EffectiveSettingsDto? EffectiveSettings, |
| 447 | string? ReindexState, |
| 448 | string? ReindexStartedAtIso); |
| 449 | |
| 450 | private sealed record EffectiveSettingsDto( |
| 451 | bool IncludeCsInFts, |
| 452 | List<string> ExtraIncludeRoots, |
| 453 | List<string> ExcludeRoots, |
| 454 | List<string> EffectiveExtensions, |
| 455 | List<string> ExcludePathSegments, |
| 456 | List<string> IgnoreFiles, |
| 457 | long MaxIndexedFileBytes, |
| 458 | int ChunkLines, |
| 459 | int ChunkOverlapLines, |
| 460 | int BinaryProbeBytes); |
| 461 | |
| 462 | private sealed record ReindexResultDto( |
| 463 | int IndexFormatVersion, |
| 464 | string DatabasePath, |
| 465 | int FilesIndexed, |
| 466 | int FilesSkippedTooLarge, |
| 467 | int FilesSkippedBinary, |
| 468 | int FilesSkippedExcluded, |
| 469 | IReadOnlyDictionary<string, int> SkippedReasonCounts, |
| 470 | List<TopPrefixDto> SkippedTopPathPrefixes, |
| 471 | List<SkippedDto> SkippedSample, |
| 472 | long DurationMs); |
| 473 | |
| 474 | private sealed record TopPrefixDto( |
| 475 | string PathPrefix, |
| 476 | int Count); |
| 477 | |
| 478 | private sealed record SkippedDto( |
| 479 | string Path, |
| 480 | string Reason); |
| 481 | |
| 482 | private sealed record VersionResultDto( |
| 483 | string AssemblyName, |
| 484 | string? AssemblyVersion, |
| 485 | string? InformationalVersion, |
| 486 | string FrameworkDescription, |
| 487 | string RuntimeIdentifier, |
| 488 | string OsDescription, |
| 489 | string ProcessArchitecture); |
| 490 | |
| 491 | private sealed record VerifyResultDto( |
| 492 | string WorkspacePath, |
| 493 | string? SolutionPath, |
| 494 | List<string> Extensions, |
| 495 | string? PathPrefix, |
| 496 | List<string>? ExcludePathPrefixes, |
| 497 | int TopN, |
| 498 | int Suggestions, |
| 499 | List<VerifyItemDto> Items); |
| 500 | |
| 501 | private sealed record VerifyItemDto( |
| 502 | string Identifier, |
| 503 | bool Exists, |
| 504 | string MatchKind, |
| 505 | string? Segment, |
| 506 | string? Err, |
| 507 | List<HitDto> Hits, |
| 508 | List<HitDto> Suggestions); |
| 509 | |
| 510 | private sealed record DraftDocDto( |
| 511 | string? Err, |
| 512 | int IndexFormatVersion, |
| 513 | string DatabasePath, |
| 514 | string Title, |
| 515 | string Markdown); |
| 516 | |
| 517 | private static string HandleVecReindex(IReadOnlyDictionary<string, JsonElement> args) |
| 518 | { |
| 519 | var ws = RequireString(args, "workspace_path"); |
| 520 | var sln = OptionalString(args, "solution_path"); |
| 521 | var (count, err) = Service.ReindexVectorsAsync(ws, sln).GetAwaiter().GetResult(); |
| 522 | return JsonSerializer.Serialize(new |
| 523 | { |
| 524 | workspacePath = ws, |
| 525 | solutionPath = sln, |
| 526 | vectorsUpserted = count, |
| 527 | err, |
| 528 | }, JsonOut); |
| 529 | } |
| 530 | |
| 531 | private static double? OptionalDouble(IReadOnlyDictionary<string, JsonElement> args, string key) |
| 532 | { |
| 533 | if (!args.TryGetValue(key, out var el)) |
| 534 | return null; |
| 535 | if (el.ValueKind == JsonValueKind.Number && el.TryGetDouble(out var d)) |
| 536 | return d; |
| 537 | return null; |
| 538 | } |
| 539 | } |
| 540 | |