cortex-local 2.2.1__tar.gz → 2.3.0__tar.gz

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 (22) hide show
  1. {cortex_local-2.2.1 → cortex_local-2.3.0}/PKG-INFO +1 -1
  2. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/chunker.py +108 -7
  3. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/code_graph.py +40 -0
  4. cortex_local-2.3.0/cortex/core/roslyn_analyzer.py +35 -0
  5. cortex_local-2.3.0/cortex/core/roslyn_tool/CortexAnalyzer.csproj +13 -0
  6. cortex_local-2.3.0/cortex/core/roslyn_tool/Program.cs +118 -0
  7. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/indexer.py +12 -3
  8. {cortex_local-2.2.1 → cortex_local-2.3.0}/README.md +0 -0
  9. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/__init__.py +0 -0
  10. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/cli.py +0 -0
  11. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/config.py +0 -0
  12. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/__init__.py +0 -0
  13. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/cache.py +0 -0
  14. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/formatters.py +0 -0
  15. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/global_graph.py +0 -0
  16. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/graph.py +0 -0
  17. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/core/notes_graph.py +0 -0
  18. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/embedder.py +0 -0
  19. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/mcp_server.py +0 -0
  20. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/state.py +0 -0
  21. {cortex_local-2.2.1 → cortex_local-2.3.0}/cortex/watcher.py +0 -0
  22. {cortex_local-2.2.1 → cortex_local-2.3.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cortex-local
3
- Version: 2.2.1
3
+ Version: 2.3.0
4
4
  Summary: Local semantic search MCP server — notes + code, embedded Qdrant, fastembed
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: click>=8.0.0
@@ -3,18 +3,44 @@
3
3
 
4
4
  import fnmatch
5
5
  import os
6
+ import re as _re
6
7
  from pathlib import Path
7
8
 
9
+ _RAZOR_BLOCK_RE = _re.compile(r'@(?:code|functions)\s*\{', _re.IGNORECASE)
10
+
11
+
12
+ def _extract_razor_cs_blocks(source: str) -> list:
13
+ """Extract @code{} and @functions{} C# blocks from Razor markup using brace counting."""
14
+ blocks = []
15
+ for match in _RAZOR_BLOCK_RE.finditer(source):
16
+ open_pos = source.index('{', match.start())
17
+ depth = 0
18
+ j = open_pos
19
+ while j < len(source):
20
+ if source[j] == '{':
21
+ depth += 1
22
+ elif source[j] == '}':
23
+ depth -= 1
24
+ if depth == 0:
25
+ start_line = source[:open_pos].count('\n') + 2 # line after opening {
26
+ end_line = source[:j].count('\n') + 1 # line of closing }
27
+ if end_line > start_line:
28
+ blocks.append({"start_line": start_line, "end_line": end_line})
29
+ break
30
+ j += 1
31
+ return blocks
32
+
8
33
  REPOS_DIR = Path(os.environ.get("REPOS_DIR", "/tmp/repos"))
9
34
 
10
- CODE_EXTS = {".js", ".ts", ".tsx", ".jsx", ".svelte", ".py", ".java", ".go", ".rs", ".css", ".html", ".kt", ".kts", ".gd", ".yml", ".yaml"}
11
- SKIP_DIRS = {"node_modules", ".git", "dist", "build", ".next", ".svelte-kit", "__pycache__", ".gradle", "target"}
35
+ CODE_EXTS = {".js", ".ts", ".tsx", ".jsx", ".svelte", ".py", ".java", ".go", ".rs", ".css", ".html", ".kt", ".kts", ".gd", ".yml", ".yaml", ".cs", ".razor", ".cshtml"}
36
+ SKIP_DIRS = {"node_modules", ".git", "dist", "build", ".next", ".svelte-kit", "__pycache__", ".gradle", "target", "bin", "obj"}
12
37
  LANG_MAP = {
13
38
  ".js": "javascript", ".ts": "typescript", ".tsx": "typescript",
14
39
  ".jsx": "javascript", ".svelte": "svelte", ".py": "python",
15
40
  ".java": "java", ".go": "go", ".rs": "rust", ".css": "css", ".html": "html",
16
41
  ".kt": "kotlin", ".kts": "kotlin", ".gd": "gdscript",
17
- ".yml": "yaml", ".yaml": "yaml",
42
+ ".yml": "yaml", ".yaml": "yaml", ".cs": "csharp",
43
+ ".razor": "razor", ".cshtml": "cshtml",
18
44
  }
19
45
 
20
46
  def _is_excluded(rel: str, patterns: list) -> bool:
@@ -97,6 +123,15 @@ _FRAMEWORK_IMPORTS = [
97
123
  ("use actix", "actix"),
98
124
  ("use axum", "axum"),
99
125
  ("use tokio", "tokio"),
126
+ # C#
127
+ ("using Microsoft.AspNetCore", "aspnetcore"),
128
+ ("using Microsoft.Extensions", "aspnetcore"),
129
+ ("using Microsoft.EntityFrameworkCore", "entityframework"),
130
+ ("using Newtonsoft.Json", "newtonsoft"),
131
+ ("using System.Text.Json", "system-text-json"),
132
+ ("using Xunit", "xunit"),
133
+ ("using NUnit.Framework", "nunit"),
134
+ ("using Microsoft.Azure", "azure-sdk"),
100
135
  ]
101
136
 
102
137
  try:
@@ -124,6 +159,15 @@ try:
124
159
  ".kt": {"function_declaration", "class_declaration", "object_declaration"},
125
160
  ".kts": {"function_declaration", "class_declaration", "object_declaration"},
126
161
  }
162
+ try:
163
+ import tree_sitter_c_sharp as _tscs
164
+ _TS_LANGUAGES[".cs"] = Language(_tscs.language())
165
+ _TS_SEMANTIC[".cs"] = {
166
+ "method_declaration", "class_declaration", "interface_declaration",
167
+ "constructor_declaration", "struct_declaration", "enum_declaration",
168
+ }
169
+ except Exception:
170
+ pass
127
171
  _TS_AVAILABLE = True
128
172
  except Exception:
129
173
  _TS_AVAILABLE = False
@@ -198,15 +242,17 @@ def _collect_semantic_nodes(node, target_types: set, depth: int = 0, max_depth:
198
242
  return result
199
243
 
200
244
 
201
- def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_url_base: str = "") -> list:
245
+ def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_url_base: str = "", roslyn_data: dict | None = None) -> list:
202
246
  """Chunk a source file into indexable segments.
203
247
 
204
248
  base_dir: directory to compute relative paths from (defaults to path.parent).
205
249
  github_url_base: e.g. "https://github.com/owner/repo/blob/master"
250
+ roslyn_data: per-file Roslyn analysis dict with "symbols" and "typeRefs" keys.
206
251
  """
207
252
  try:
208
- source = path.read_bytes()
209
- lines = source.decode("utf-8", errors="replace").splitlines()
253
+ raw = path.read_bytes()
254
+ source_text = raw.decode("utf-8", errors="replace")
255
+ lines = source_text.splitlines()
210
256
  except Exception:
211
257
  return []
212
258
  if not lines:
@@ -225,6 +271,35 @@ def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_
225
271
  h = f"# {repo_name}/{rel} (lines {s}-{e})\n# {tokens}"
226
272
  return h + f"\n# {extra}" if extra else h
227
273
 
274
+ # Razor/Blazor: extract @code blocks as csharp + whole file as razor context
275
+ if ext in (".razor", ".cshtml"):
276
+ chunks = []
277
+ blocks = _extract_razor_cs_blocks(source_text)
278
+ for block in blocks:
279
+ s, e = block["start_line"], block["end_line"]
280
+ body = "\n".join(lines[s - 1:e]).strip()
281
+ if body:
282
+ url = f"{github_url_base}/{rel}#L{s}-L{e}" if github_url_base else ""
283
+ tokens = _path_tokens(repo_name, rel)
284
+ chunks.append({
285
+ "repo": repo_name, "file": rel, "language": "csharp",
286
+ "start_line": s, "end_line": e,
287
+ "text": f"{_header(rel, s, e, tokens)}\n\n{body}",
288
+ "github_url": url,
289
+ })
290
+ # Always include a full-file razor chunk for markup context
291
+ body = "\n".join(lines).strip()
292
+ if body:
293
+ url = f"{github_url_base}/{rel}#L1-L{len(lines)}" if github_url_base else ""
294
+ tokens = _path_tokens(repo_name, rel)
295
+ chunks.append({
296
+ "repo": repo_name, "file": rel, "language": language,
297
+ "start_line": 1, "end_line": len(lines),
298
+ "text": f"{_header(rel, 1, len(lines), tokens)}\n\n{body}",
299
+ "github_url": url,
300
+ })
301
+ return chunks
302
+
228
303
  # Small files: keep as single chunk to avoid diluting path/filename signal
229
304
  if len(lines) <= MAX_CHUNK_LINES:
230
305
  body = "\n".join(lines).strip()
@@ -239,10 +314,36 @@ def chunk_file(path: Path, repo_name: str, base_dir: Path | None = None, github_
239
314
  }]
240
315
  return []
241
316
 
317
+ # Roslyn semantic chunking for .cs (preferred over tree-sitter when available)
318
+ if roslyn_data and ext == ".cs":
319
+ top_level_kinds = {"class", "interface", "struct", "enum", "record"}
320
+ all_syms = roslyn_data.get("symbols", [])
321
+ syms = [s for s in all_syms if s.get("kind") in top_level_kinds] or all_syms
322
+ if syms:
323
+ chunks = []
324
+ for sym in syms:
325
+ s = sym["startLine"] - 1 # 0-indexed
326
+ e = sym["endLine"]
327
+ if e - s > MAX_CHUNK_LINES:
328
+ chunks.extend(_sliding_window(lines, s, e, repo_name, rel, language, github_url_base, extra))
329
+ else:
330
+ body = "\n".join(lines[s:e]).strip()
331
+ if body:
332
+ url = f"{github_url_base}/{rel}#L{s+1}-L{e}" if github_url_base else ""
333
+ tokens = _path_tokens(repo_name, rel)
334
+ chunks.append({
335
+ "repo": repo_name, "file": rel, "language": language,
336
+ "start_line": s + 1, "end_line": e,
337
+ "text": f"{_header(rel, s+1, e, tokens)}\n\n{body}",
338
+ "github_url": url,
339
+ })
340
+ if chunks:
341
+ return chunks
342
+
242
343
  if _TS_AVAILABLE and ext in _TS_LANGUAGES:
243
344
  try:
244
345
  parser = _TSParser(_TS_LANGUAGES[ext])
245
- tree = parser.parse(source)
346
+ tree = parser.parse(raw)
246
347
  nodes = _collect_semantic_nodes(tree.root_node, _TS_SEMANTIC[ext])
247
348
  nodes = [n for n in nodes if n.type != "lexical_declaration" or _has_function_value(n)]
248
349
  if nodes:
@@ -17,6 +17,7 @@ _IMPORT_RE: dict = {
17
17
  "js": re.compile(r'''(?:import|from|require)\s*\(?['"](\.[^'"]+|\$lib/[^'"]+)['"]'''),
18
18
  "ts": re.compile(r'''(?:import|from|require)\s*\(?['"](\.[^'"]+|\$lib/[^'"]+)['"]'''),
19
19
  "kt": re.compile(r'^import\s+([\w.]+)', re.MULTILINE),
20
+ "cs": re.compile(r'^using\s+(?!static\b)(?:\w+\s*=\s*)?(\w+(?:\.\w+)+)\s*;', re.MULTILINE),
20
21
  }
21
22
 
22
23
  _NAMED_IMPORT_RE = re.compile(
@@ -31,6 +32,7 @@ _TOP_LEVEL_DEF_RE: dict = {
31
32
  "py": re.compile(r'^(?:class|def)\s+(\w+)', re.MULTILINE),
32
33
  "ts": re.compile(r'(?:export\s+)?(?:class|function|const|interface|type)\s+(\w+)'),
33
34
  "kt": re.compile(r'(?:class|fun|object)\s+(\w+)'),
35
+ "cs": re.compile(r'(?m)(?:class|interface|struct|enum|record)\s+(\w+)'),
34
36
  }
35
37
 
36
38
  _INHERIT_RE: dict = {
@@ -42,12 +44,17 @@ _INHERIT_RE: dict = {
42
44
  r'(?:class|interface|object)\s+\w+[^:{]*:\s*([\w,\s()]+?)(?:\{|$)',
43
45
  re.MULTILINE,
44
46
  ),
47
+ "cs": re.compile(
48
+ r'(?:class|struct|record)\s+\w+(?:<[^>]*>)?\s*:\s*([\w,\s<>.?[\]]+?)(?=\s*(?:where|\{))',
49
+ re.MULTILINE,
50
+ ),
45
51
  }
46
52
 
47
53
  _LANG_KEY: dict = {
48
54
  ".py": "py", ".js": "js", ".jsx": "js",
49
55
  ".ts": "ts", ".tsx": "ts", ".svelte": "js",
50
56
  ".kt": "kt", ".kts": "kt",
57
+ ".cs": "cs", ".razor": "cs", ".cshtml": "cs",
51
58
  }
52
59
 
53
60
  _EDGE_PRIORITY: dict = {"import": 0, "call": 1, "inherits": 2}
@@ -62,6 +69,12 @@ _PY_SKIP_BASES = frozenset({
62
69
  "list", "dict", "set", "tuple", "Enum", "IntEnum",
63
70
  })
64
71
  _TS_SKIP_BASES = frozenset({"Error", "EventEmitter", "Component", "React"})
72
+ _CS_SKIP_BASES = frozenset({
73
+ "object", "Object", "Exception", "SystemException", "ApplicationException",
74
+ "Attribute", "Controller", "ControllerBase", "ApiController", "PageModel",
75
+ "BackgroundService", "IHostedService", "IDisposable", "ValueType",
76
+ "IEnumerable", "IList", "ICollection", "IQueryable", "IComparable",
77
+ })
65
78
 
66
79
 
67
80
  def _extract_imports(path: Path, ext: str) -> list:
@@ -217,6 +230,14 @@ def _extract_inheritance_targets(
217
230
  t = symbol_to_file.get(base)
218
231
  if t and t != rel:
219
232
  targets.add(t)
233
+ elif key == "cs":
234
+ for m in _INHERIT_RE["cs"].finditer(src):
235
+ for base in m.group(1).split(","):
236
+ base = base.strip().split("<")[0].strip()
237
+ if base and base not in _CS_SKIP_BASES:
238
+ t = symbol_to_file.get(base)
239
+ if t and t != rel:
240
+ targets.add(t)
220
241
 
221
242
  return list(targets)
222
243
 
@@ -230,6 +251,25 @@ def _add_edge(G, src: str, dst: str, edge_type: str) -> None:
230
251
  G.add_edge(src, dst, type=edge_type)
231
252
 
232
253
 
254
+ def inject_roslyn_edges(G, roslyn_data: dict, all_files: set) -> None:
255
+ """Add cross-file type reference edges from Roslyn analysis output."""
256
+ if not _NX_AVAILABLE or G is None or not roslyn_data:
257
+ return
258
+ symbol_to_file: dict = {}
259
+ for rel, data in roslyn_data.items():
260
+ for sym in data.get("symbols", []):
261
+ name = sym.get("name", "")
262
+ if name and name not in symbol_to_file:
263
+ symbol_to_file[name] = rel
264
+ for rel, data in roslyn_data.items():
265
+ if rel not in all_files or not G.has_node(rel):
266
+ continue
267
+ for type_ref in data.get("typeRefs", []):
268
+ target = symbol_to_file.get(type_ref)
269
+ if target and target != rel and target in all_files and G.has_node(target):
270
+ _add_edge(G, rel, target, "import")
271
+
272
+
233
273
  def build_code_graph(repo_path: Path, all_files: set):
234
274
  if not _NX_AVAILABLE:
235
275
  return None
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env python3
2
+ """Python wrapper for the Roslyn C# syntax analyzer subprocess."""
3
+
4
+ import json
5
+ import os
6
+ import subprocess
7
+ from pathlib import Path
8
+
9
+ ROSLYN_BINARY = Path(os.environ.get(
10
+ "ROSLYN_ANALYZER",
11
+ str(Path(__file__).parent / "roslyn_tool" / "CortexAnalyzer")
12
+ ))
13
+
14
+
15
+ def is_available() -> bool:
16
+ return ROSLYN_BINARY.exists() and os.access(ROSLYN_BINARY, os.X_OK)
17
+
18
+
19
+ def analyze_dir(dir_path: Path) -> dict:
20
+ """Run Roslyn analyzer on a directory. Returns per-file symbol + type_ref data.
21
+
22
+ Keys are relative paths (forward slashes). Returns {} on any failure.
23
+ """
24
+ if not is_available():
25
+ return {}
26
+ try:
27
+ result = subprocess.run(
28
+ [str(ROSLYN_BINARY), str(dir_path)],
29
+ capture_output=True, text=True, timeout=60,
30
+ )
31
+ if result.returncode != 0:
32
+ return {}
33
+ return json.loads(result.stdout)
34
+ except Exception:
35
+ return {}
@@ -0,0 +1,13 @@
1
+ <Project Sdk="Microsoft.NET.Sdk">
2
+ <PropertyGroup>
3
+ <OutputType>Exe</OutputType>
4
+ <TargetFramework>net8.0</TargetFramework>
5
+ <Nullable>enable</Nullable>
6
+ <ImplicitUsings>enable</ImplicitUsings>
7
+ <AssemblyName>CortexAnalyzer</AssemblyName>
8
+ <RootNamespace>CortexAnalyzer</RootNamespace>
9
+ </PropertyGroup>
10
+ <ItemGroup>
11
+ <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
12
+ </ItemGroup>
13
+ </Project>
@@ -0,0 +1,118 @@
1
+ using Microsoft.CodeAnalysis.CSharp;
2
+ using Microsoft.CodeAnalysis.CSharp.Syntax;
3
+ using System.Text.Json;
4
+
5
+ var dir = args.Length > 0 ? args[0] : Directory.GetCurrentDirectory();
6
+ var jOpts = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
7
+ var results = new Dictionary<string, FileResult>();
8
+
9
+ foreach (var file in Directory.EnumerateFiles(dir, "*.cs", SearchOption.AllDirectories))
10
+ {
11
+ var rel = Path.GetRelativePath(dir, file).Replace('\\', '/');
12
+ try
13
+ {
14
+ var source = File.ReadAllText(file);
15
+ var tree = CSharpSyntaxTree.ParseText(source);
16
+ var root = tree.GetRoot();
17
+
18
+ var symbols = new List<SymbolInfo>();
19
+ var typeRefs = new HashSet<string>();
20
+
21
+ foreach (var node in root.DescendantNodes())
22
+ {
23
+ var span = tree.GetLineSpan(node.Span);
24
+ int s = span.StartLinePosition.Line + 1;
25
+ int e = span.EndLinePosition.Line + 1;
26
+
27
+ switch (node)
28
+ {
29
+ case ClassDeclarationSyntax c:
30
+ symbols.Add(new(c.Identifier.Text, "class", s, e));
31
+ AddBaseTypes(typeRefs, c.BaseList);
32
+ break;
33
+ case InterfaceDeclarationSyntax i:
34
+ symbols.Add(new(i.Identifier.Text, "interface", s, e));
35
+ AddBaseTypes(typeRefs, i.BaseList);
36
+ break;
37
+ case RecordDeclarationSyntax r:
38
+ symbols.Add(new(r.Identifier.Text, "record", s, e));
39
+ AddBaseTypes(typeRefs, r.BaseList);
40
+ break;
41
+ case StructDeclarationSyntax st:
42
+ symbols.Add(new(st.Identifier.Text, "struct", s, e));
43
+ AddBaseTypes(typeRefs, st.BaseList);
44
+ break;
45
+ case EnumDeclarationSyntax en:
46
+ symbols.Add(new(en.Identifier.Text, "enum", s, e));
47
+ break;
48
+ case MethodDeclarationSyntax m:
49
+ symbols.Add(new(m.Identifier.Text, "method", s, e));
50
+ AddTypeName(typeRefs, m.ReturnType);
51
+ foreach (var p in m.ParameterList.Parameters)
52
+ if (p.Type is not null) AddTypeName(typeRefs, p.Type);
53
+ break;
54
+ case ConstructorDeclarationSyntax ctor:
55
+ symbols.Add(new(ctor.Identifier.Text, "constructor", s, e));
56
+ foreach (var p in ctor.ParameterList.Parameters)
57
+ if (p.Type is not null) AddTypeName(typeRefs, p.Type);
58
+ break;
59
+ case PropertyDeclarationSyntax prop:
60
+ symbols.Add(new(prop.Identifier.Text, "property", s, e));
61
+ AddTypeName(typeRefs, prop.Type);
62
+ break;
63
+ case FieldDeclarationSyntax f:
64
+ AddTypeName(typeRefs, f.Declaration.Type);
65
+ break;
66
+ case ObjectCreationExpressionSyntax obj:
67
+ AddTypeName(typeRefs, obj.Type);
68
+ break;
69
+ }
70
+ }
71
+
72
+ results[rel] = new FileResult(symbols, typeRefs.ToList());
73
+ }
74
+ catch { /* skip unparseable files */ }
75
+ }
76
+
77
+ Console.WriteLine(JsonSerializer.Serialize(results, jOpts));
78
+
79
+ void AddBaseTypes(HashSet<string> refs, BaseListSyntax? baseList)
80
+ {
81
+ if (baseList is null) return;
82
+ foreach (var bt in baseList.Types)
83
+ AddTypeName(refs, bt.Type);
84
+ }
85
+
86
+ void AddTypeName(HashSet<string> refs, TypeSyntax type)
87
+ {
88
+ switch (type)
89
+ {
90
+ case IdentifierNameSyntax id when !IsPrimitive(id.Identifier.Text):
91
+ refs.Add(id.Identifier.Text);
92
+ break;
93
+ case GenericNameSyntax gen:
94
+ if (!IsPrimitive(gen.Identifier.Text)) refs.Add(gen.Identifier.Text);
95
+ foreach (var arg in gen.TypeArgumentList.Arguments) AddTypeName(refs, arg);
96
+ break;
97
+ case QualifiedNameSyntax q:
98
+ AddTypeName(refs, q.Right);
99
+ break;
100
+ case NullableTypeSyntax n:
101
+ AddTypeName(refs, n.ElementType);
102
+ break;
103
+ case ArrayTypeSyntax a:
104
+ AddTypeName(refs, a.ElementType);
105
+ break;
106
+ }
107
+ }
108
+
109
+ bool IsPrimitive(string n) => n is
110
+ "void" or "string" or "int" or "long" or "double" or "float" or "bool"
111
+ or "byte" or "char" or "object" or "decimal" or "short" or "uint" or "ulong"
112
+ or "ushort" or "sbyte" or "nint" or "nuint" or "var" or "dynamic"
113
+ or "String" or "Int32" or "Int64" or "Boolean" or "Double" or "Object" or "Void"
114
+ or "Byte" or "Char" or "Decimal" or "Single" or "DateTime" or "DateTimeOffset"
115
+ or "Guid" or "TimeSpan" or "Uri";
116
+
117
+ record SymbolInfo(string Name, string Kind, int StartLine, int EndLine);
118
+ record FileResult(List<SymbolInfo> Symbols, List<string> TypeRefs);
@@ -17,6 +17,7 @@ from .embedder import embed, sparse_embed
17
17
  from .core.chunker import chunk_file, CODE_EXTS, SKIP_DIRS, _is_excluded
18
18
  from .core.cache import load_cache, save_cache
19
19
  from .core import graph as _graph
20
+ from .core import roslyn_analyzer as _roslyn
20
21
 
21
22
 
22
23
  def _qdrant() -> QdrantClient:
@@ -199,8 +200,13 @@ def index_code(code_dir: Path, show_progress: bool = False) -> int:
199
200
  cached = 0
200
201
  file_point_ids: dict = {}
201
202
 
203
+ roslyn_data: dict = {}
204
+ if _roslyn.is_available() and any(p.suffix == ".cs" for p in code_files):
205
+ roslyn_data = _roslyn.analyze_dir(code_dir)
206
+
202
207
  for path in tqdm(code_files, desc=repo_name, disable=not show_progress):
203
- cache_key = f"{repo_name}/{str(path.relative_to(code_dir))}"
208
+ rel_path = str(path.relative_to(code_dir)).replace("\\", "/")
209
+ cache_key = f"{repo_name}/{rel_path}"
204
210
  mtime = path.stat().st_mtime
205
211
  entry = cache.get(cache_key, {})
206
212
  if entry.get("mtime") == mtime:
@@ -213,7 +219,7 @@ def index_code(code_dir: Path, show_progress: bool = False) -> int:
213
219
  continue
214
220
 
215
221
  points = []
216
- for chunk in chunk_file(path, repo_name, base_dir=code_dir):
222
+ for chunk in chunk_file(path, repo_name, base_dir=code_dir, roslyn_data=roslyn_data.get(rel_path)):
217
223
  pid = _code_chunk_id(repo_name, chunk["file"], chunk["start_line"])
218
224
  points.append(_make_point(chunk["text"], pid, chunk, use_sparse))
219
225
  file_point_ids.setdefault(chunk["file"], []).append(pid)
@@ -226,10 +232,13 @@ def index_code(code_dir: Path, show_progress: bool = False) -> int:
226
232
  print(f" {repo_name}: {total} chunks from {len(code_files) - cached} files ({cached} cached)", flush=True)
227
233
 
228
234
  all_files = {
229
- str(p.relative_to(code_dir)) for p in code_files
235
+ str(p.relative_to(code_dir)).replace("\\", "/") for p in code_files
230
236
  }
231
237
  try:
232
238
  G = _graph.build_code_graph(code_dir, all_files)
239
+ if roslyn_data:
240
+ from .core.code_graph import inject_roslyn_edges
241
+ inject_roslyn_edges(G, roslyn_data, all_files)
233
242
  if G is not None:
234
243
  metadata = _graph.compute_code_metadata(G)
235
244
  _graph.persist_code_graph(G, metadata, repo_name, data_dir())
File without changes
File without changes