sia-code 0.2.2__tar.gz → 0.2.3__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 (37) hide show
  1. {sia_code-0.2.2 → sia_code-0.2.3}/PKG-INFO +1 -1
  2. {sia_code-0.2.2 → sia_code-0.2.3}/pyproject.toml +2 -2
  3. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/__init__.py +1 -1
  4. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/cli.py +12 -2
  5. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/indexer/coordinator.py +1 -110
  6. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/parser/concepts.py +151 -0
  7. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/parser/engine.py +10 -3
  8. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/search/entity_extractor.py +7 -2
  9. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/search/multi_hop.py +7 -1
  10. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code.egg-info/PKG-INFO +1 -1
  11. {sia_code-0.2.2 → sia_code-0.2.3}/tests/test_basic.py +24 -3
  12. {sia_code-0.2.2 → sia_code-0.2.3}/README.md +0 -0
  13. {sia_code-0.2.2 → sia_code-0.2.3}/setup.cfg +0 -0
  14. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/config.py +0 -0
  15. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/core/__init__.py +0 -0
  16. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/core/models.py +0 -0
  17. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/core/types.py +0 -0
  18. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/indexer/__init__.py +0 -0
  19. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/indexer/chunk_index.py +0 -0
  20. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/indexer/embedder.py +0 -0
  21. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/indexer/hash_cache.py +0 -0
  22. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/indexer/metrics.py +0 -0
  23. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/parser/__init__.py +0 -0
  24. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/parser/chunker.py +0 -0
  25. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/parser/languages/__init__.py +0 -0
  26. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/search/__init__.py +0 -0
  27. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/search/service.py +0 -0
  28. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/search/single_hop.py +0 -0
  29. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/storage/__init__.py +0 -0
  30. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code/storage/backend.py +0 -0
  31. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code.egg-info/SOURCES.txt +0 -0
  32. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code.egg-info/dependency_links.txt +0 -0
  33. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code.egg-info/entry_points.txt +0 -0
  34. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code.egg-info/requires.txt +0 -0
  35. {sia_code-0.2.2 → sia_code-0.2.3}/sia_code.egg-info/top_level.txt +0 -0
  36. {sia_code-0.2.2 → sia_code-0.2.3}/tests/test_cli_integration.py +0 -0
  37. {sia_code-0.2.2 → sia_code-0.2.3}/tests/test_empty_code_fix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sia-code
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Local-first codebase intelligence with semantic search, multi-hop research, and 12-language AST support
5
5
  Author: Sia Code Contributors
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sia-code"
3
- version = "0.2.2"
3
+ version = "0.2.3"
4
4
  description = "Local-first codebase intelligence with semantic search, multi-hop research, and 12-language AST support"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -98,7 +98,7 @@ python_classes = "Test*"
98
98
  python_functions = "test_*"
99
99
 
100
100
  [tool.bumpversion]
101
- current_version = "0.2.2"
101
+ current_version = "0.2.3"
102
102
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
103
103
  serialize = ["{major}.{minor}.{patch}"]
104
104
  commit = true
@@ -3,5 +3,5 @@
3
3
  Semantic search, multi-hop research, and 12-language AST support.
4
4
  """
5
5
 
6
- __version__ = "0.2.2"
6
+ __version__ = "0.2.3"
7
7
  __all__ = ["__version__"]
@@ -381,14 +381,24 @@ def index(
381
381
  console.print("[dim]Re-indexing...[/dim]")
382
382
 
383
383
  try:
384
- # Perform incremental reindex
384
+ # Perform incremental reindex using v2
385
385
  from .indexer.hash_cache import HashCache
386
+ from .indexer.chunk_index import ChunkIndex
386
387
 
387
388
  cache_path = sia_dir / "cache" / "file_hashes.json"
388
389
  cache = HashCache(cache_path)
389
390
 
391
+ chunk_index_path = sia_dir / "chunk_index.json"
392
+ chunk_index = ChunkIndex(chunk_index_path)
393
+ chunk_index.load()
394
+
390
395
  coordinator = IndexingCoordinator(backend=backend, config=config)
391
- stats = coordinator.index_directory_incremental(Path(path), cache)
396
+ stats = coordinator.index_directory_incremental_v2(
397
+ Path(path), cache, chunk_index, progress_callback=None
398
+ )
399
+
400
+ # Save updated chunk index
401
+ chunk_index.save()
392
402
 
393
403
  console.print(
394
404
  f"[green]✓[/green] Re-indexed {stats['files_indexed']} files, {stats['chunks_indexed']} chunks"
@@ -1,12 +1,11 @@
1
1
  """Indexing coordinator - orchestrates parse → chunk → store."""
2
2
 
3
3
  from pathlib import Path
4
- import hashlib
5
4
  import logging
6
5
  import time
7
6
  import os
8
7
  from concurrent.futures import ProcessPoolExecutor, as_completed
9
- from typing import cast, Callable
8
+ from typing import Callable
10
9
 
11
10
  import pathspec
12
11
 
@@ -485,99 +484,6 @@ class IndexingCoordinator:
485
484
 
486
485
  return stats
487
486
 
488
- def index_directory_incremental(self, directory: Path, cache: HashCache) -> dict:
489
- """Index only changed files using hash cache.
490
-
491
- Args:
492
- directory: Root directory to index
493
- cache: Hash cache for change detection
494
-
495
- Returns:
496
- Statistics dictionary
497
- """
498
- # Start performance tracking
499
- metrics = PerformanceMetrics()
500
-
501
- files = self._discover_files(directory)
502
-
503
- stats = {
504
- "total_files": len(files),
505
- "changed_files": 0,
506
- "skipped_files": 0,
507
- "indexed_files": 0,
508
- "total_chunks": 0,
509
- "errors": [],
510
- "metrics": None, # Will be filled at end
511
- }
512
-
513
- for file_path in files:
514
- # Check if file changed
515
- if not cache.has_changed(file_path):
516
- stats["skipped_files"] += 1
517
- continue
518
-
519
- stats["changed_files"] += 1
520
-
521
- try:
522
- language = Language.from_extension(file_path.suffix)
523
-
524
- if not self.chunker.engine.is_supported(language):
525
- logger.debug(f"Skipping unsupported language: {file_path}")
526
- continue
527
-
528
- # Get old chunk IDs for this file
529
- old_chunk_ids = cache.get_chunks(file_path)
530
-
531
- # Delete old chunks (placeholder - Memvid doesn't support direct delete)
532
- if old_chunk_ids:
533
- logger.debug(
534
- f"Old chunks exist for {file_path} ({len(old_chunk_ids)}), "
535
- "but cannot delete (Memvid limitation)"
536
- )
537
-
538
- # Chunk the file with retry
539
- chunks, error = self._index_file_with_retry(file_path, language)
540
-
541
- if error:
542
- stats["errors"].append(f"{file_path}: {error}")
543
- metrics.errors_count += 1
544
- continue
545
-
546
- if chunks:
547
- # Track file size
548
- try:
549
- metrics.bytes_processed += file_path.stat().st_size
550
- except OSError:
551
- pass
552
-
553
- # Store new chunks
554
- chunk_ids = self.backend.store_chunks_batch(chunks)
555
-
556
- # Update cache with new chunk IDs
557
- cache.update(file_path, cast(list[str], chunk_ids))
558
-
559
- stats["indexed_files"] += 1
560
- stats["total_chunks"] += len(chunks)
561
- metrics.files_processed += 1
562
- metrics.chunks_created += len(chunks)
563
- logger.info(f"Re-indexed {file_path}: {len(chunks)} chunks")
564
-
565
- except Exception as e:
566
- error_msg = f"Unexpected error: {str(e)}"
567
- stats["errors"].append(f"{file_path}: {error_msg}")
568
- metrics.errors_count += 1
569
- logger.exception(f"Unexpected error indexing {file_path}")
570
-
571
- # Save updated cache
572
- cache.save()
573
-
574
- # Finalize metrics
575
- metrics.finish()
576
- stats["metrics"] = metrics.to_dict()
577
- logger.info(f"Incremental indexing complete: {metrics}")
578
-
579
- return stats
580
-
581
487
  def compact_index(
582
488
  self, directory: Path, chunk_index: ChunkIndex, threshold: float = 0.2
583
489
  ) -> dict:
@@ -722,18 +628,3 @@ class IndexingCoordinator:
722
628
  logger.info(f"Compaction complete: {metrics}")
723
629
 
724
630
  return stats
725
-
726
- def get_file_hash(self, file_path: Path) -> str:
727
- """Calculate file hash for change detection.
728
-
729
- Args:
730
- file_path: Path to file
731
-
732
- Returns:
733
- MD5 hash of file
734
- """
735
- hasher = hashlib.md5()
736
- with open(file_path, "rb") as f:
737
- for chunk in iter(lambda: f.read(4096), b""):
738
- hasher.update(chunk)
739
- return hasher.hexdigest()
@@ -134,6 +134,157 @@ class ConceptExtractor:
134
134
  traverse(root)
135
135
  return concepts
136
136
 
137
+ def _extract_javascript_concepts(
138
+ self, root: Node, source_code: bytes
139
+ ) -> list[UniversalConcept]:
140
+ """Extract JavaScript/TypeScript-specific concepts."""
141
+ concepts = []
142
+
143
+ def traverse(node: Node, parent_class: str | None = None):
144
+ # Function declarations: function foo() {}
145
+ if node.type == "function_declaration":
146
+ name_node = node.child_by_field_name("name")
147
+ if name_node:
148
+ symbol = source_code[name_node.start_byte : name_node.end_byte].decode("utf-8")
149
+ concepts.append(
150
+ UniversalConcept(
151
+ concept_type=ConceptType.DEFINITION,
152
+ chunk_type=ChunkType.FUNCTION,
153
+ symbol=symbol,
154
+ start_line=LineNumber(node.start_point[0] + 1),
155
+ end_line=LineNumber(node.end_point[0] + 1),
156
+ start_byte=ByteOffset(node.start_byte),
157
+ end_byte=ByteOffset(node.end_byte),
158
+ code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
159
+ parent_header=parent_class,
160
+ )
161
+ )
162
+
163
+ # Arrow functions and function expressions: const foo = () => {}
164
+ elif node.type in ("arrow_function", "function_expression", "function"):
165
+ # Try to find the parent variable declarator to get the name
166
+ parent = node.parent
167
+ symbol = "anonymous"
168
+ if parent and parent.type == "variable_declarator":
169
+ name_node = parent.child_by_field_name("name")
170
+ if name_node:
171
+ symbol = source_code[name_node.start_byte : name_node.end_byte].decode(
172
+ "utf-8"
173
+ )
174
+
175
+ concepts.append(
176
+ UniversalConcept(
177
+ concept_type=ConceptType.DEFINITION,
178
+ chunk_type=ChunkType.FUNCTION,
179
+ symbol=symbol,
180
+ start_line=LineNumber(node.start_point[0] + 1),
181
+ end_line=LineNumber(node.end_point[0] + 1),
182
+ start_byte=ByteOffset(node.start_byte),
183
+ end_byte=ByteOffset(node.end_byte),
184
+ code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
185
+ parent_header=parent_class,
186
+ )
187
+ )
188
+
189
+ # Class declarations
190
+ elif node.type == "class_declaration":
191
+ name_node = node.child_by_field_name("name")
192
+ if name_node:
193
+ symbol = source_code[name_node.start_byte : name_node.end_byte].decode("utf-8")
194
+ concepts.append(
195
+ UniversalConcept(
196
+ concept_type=ConceptType.DEFINITION,
197
+ chunk_type=ChunkType.CLASS,
198
+ symbol=symbol,
199
+ start_line=LineNumber(node.start_point[0] + 1),
200
+ end_line=LineNumber(node.end_point[0] + 1),
201
+ start_byte=ByteOffset(node.start_byte),
202
+ end_byte=ByteOffset(node.end_byte),
203
+ code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
204
+ )
205
+ )
206
+ # Traverse children with class context
207
+ for child in node.children:
208
+ traverse(child, parent_class=symbol)
209
+ return # Don't traverse again
210
+
211
+ # Method definitions (inside classes)
212
+ elif node.type in ("method_definition", "public_field_definition"):
213
+ name_node = node.child_by_field_name("name")
214
+ if name_node:
215
+ symbol = source_code[name_node.start_byte : name_node.end_byte].decode("utf-8")
216
+ concepts.append(
217
+ UniversalConcept(
218
+ concept_type=ConceptType.DEFINITION,
219
+ chunk_type=ChunkType.METHOD if parent_class else ChunkType.FUNCTION,
220
+ symbol=symbol,
221
+ start_line=LineNumber(node.start_point[0] + 1),
222
+ end_line=LineNumber(node.end_point[0] + 1),
223
+ start_byte=ByteOffset(node.start_byte),
224
+ end_byte=ByteOffset(node.end_byte),
225
+ code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
226
+ parent_header=parent_class,
227
+ )
228
+ )
229
+
230
+ # TypeScript-specific: interface declarations
231
+ elif node.type == "interface_declaration":
232
+ name_node = node.child_by_field_name("name")
233
+ if name_node:
234
+ symbol = source_code[name_node.start_byte : name_node.end_byte].decode("utf-8")
235
+ concepts.append(
236
+ UniversalConcept(
237
+ concept_type=ConceptType.DEFINITION,
238
+ chunk_type=ChunkType.CLASS, # Treat interfaces like classes
239
+ symbol=symbol,
240
+ start_line=LineNumber(node.start_point[0] + 1),
241
+ end_line=LineNumber(node.end_point[0] + 1),
242
+ start_byte=ByteOffset(node.start_byte),
243
+ end_byte=ByteOffset(node.end_byte),
244
+ code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
245
+ )
246
+ )
247
+
248
+ # TypeScript-specific: type alias declarations
249
+ elif node.type == "type_alias_declaration":
250
+ name_node = node.child_by_field_name("name")
251
+ if name_node:
252
+ symbol = source_code[name_node.start_byte : name_node.end_byte].decode("utf-8")
253
+ concepts.append(
254
+ UniversalConcept(
255
+ concept_type=ConceptType.DEFINITION,
256
+ chunk_type=ChunkType.CLASS, # Treat type aliases like classes
257
+ symbol=symbol,
258
+ start_line=LineNumber(node.start_point[0] + 1),
259
+ end_line=LineNumber(node.end_point[0] + 1),
260
+ start_byte=ByteOffset(node.start_byte),
261
+ end_byte=ByteOffset(node.end_byte),
262
+ code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
263
+ )
264
+ )
265
+
266
+ # Comments
267
+ elif node.type == "comment":
268
+ concepts.append(
269
+ UniversalConcept(
270
+ concept_type=ConceptType.COMMENT,
271
+ chunk_type=ChunkType.COMMENT,
272
+ symbol="comment",
273
+ start_line=LineNumber(node.start_point[0] + 1),
274
+ end_line=LineNumber(node.end_point[0] + 1),
275
+ start_byte=ByteOffset(node.start_byte),
276
+ end_byte=ByteOffset(node.end_byte),
277
+ code=source_code[node.start_byte : node.end_byte].decode("utf-8"),
278
+ )
279
+ )
280
+
281
+ # Traverse children
282
+ for child in node.children:
283
+ traverse(child, parent_class)
284
+
285
+ traverse(root)
286
+ return concepts
287
+
137
288
  def _extract_generic_concepts(self, root: Node, source_code: bytes) -> list[UniversalConcept]:
138
289
  """Extract concepts from C-like languages using common node types."""
139
290
  concepts = []
@@ -100,8 +100,11 @@ class TreeSitterEngine:
100
100
  with open(file_path, "rb") as f:
101
101
  source_code = f.read()
102
102
  return self.parse_code(source_code, language)
103
- except Exception:
104
- # Silent fail for individual files
103
+ except Exception as e:
104
+ # Log parse failures for debugging
105
+ import logging
106
+
107
+ logging.getLogger(__name__).debug(f"Parse failed for {file_path}: {e}")
105
108
  return None
106
109
 
107
110
  def parse_code(self, source_code: bytes | str, language: PciLanguage):
@@ -116,7 +119,11 @@ class TreeSitterEngine:
116
119
  parser = self._parsers[language]
117
120
  tree = parser.parse(source_code)
118
121
  return tree.root_node
119
- except Exception:
122
+ except Exception as e:
123
+ # Log parse failures for debugging
124
+ import logging
125
+
126
+ logging.getLogger(__name__).debug(f"Parse code failed for {language}: {e}")
120
127
  return None
121
128
 
122
129
  def is_supported(self, language: PciLanguage) -> bool:
@@ -52,8 +52,13 @@ class EntityExtractor:
52
52
  elif chunk.language in [Language.JAVASCRIPT, Language.TYPESCRIPT, Language.TSX]:
53
53
  entities.extend(self._extract_js_entities(root, chunk))
54
54
 
55
- except Exception:
56
- # Silent fail for extraction errors
55
+ except Exception as e:
56
+ # Log extraction failures for debugging
57
+ import logging
58
+
59
+ logging.getLogger(__name__).debug(
60
+ f"Entity extraction failed for chunk {chunk.symbol}: {e}"
61
+ )
57
62
  pass
58
63
 
59
64
  return entities
@@ -107,7 +107,13 @@ class MultiHopSearchStrategy:
107
107
  # Search for this entity
108
108
  try:
109
109
  entity_results = self.backend.search_lexical(entity.name, k=3)
110
- except Exception:
110
+ except Exception as e:
111
+ # Log search failures for debugging
112
+ import logging
113
+
114
+ logging.getLogger(__name__).debug(
115
+ f"Entity search failed for {entity.name}: {e}"
116
+ )
111
117
  continue
112
118
 
113
119
  # Process results
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sia-code
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Local-first codebase intelligence with semantic search, multi-hop research, and 12-language AST support
5
5
  Author: Sia Code Contributors
6
6
  License: MIT
@@ -98,7 +98,9 @@ class TestMemvidBackend:
98
98
  backend.store_chunks_batch(sample_chunks)
99
99
 
100
100
  results = backend.search_lexical("search", k=5)
101
- assert len(results) > 0
101
+
102
+ # Respect the k parameter
103
+ assert 1 <= len(results) <= 5
102
104
 
103
105
  # Check result structure
104
106
  for result in results:
@@ -106,23 +108,42 @@ class TestMemvidBackend:
106
108
  assert result.chunk.symbol is not None
107
109
  assert result.score > 0 # BM25 scores are positive
108
110
 
111
+ # Verify results are sorted by score (descending)
112
+ scores = [r.score for r in results]
113
+ assert scores == sorted(scores, reverse=True), (
114
+ "Results should be sorted by score (descending)"
115
+ )
116
+
109
117
  def test_lexical_search_multiple_terms(self, backend, sample_chunks):
110
118
  """Test lexical search with multiple terms."""
111
119
  backend.store_chunks_batch(sample_chunks)
112
120
 
113
121
  results = backend.search_lexical("semantic code chunk", k=5)
114
- assert len(results) > 0
122
+
123
+ # Respect the k parameter
124
+ assert 1 <= len(results) <= 5
125
+
126
+ # Verify results are sorted by relevance
127
+ scores = [r.score for r in results]
128
+ assert scores == sorted(scores, reverse=True)
115
129
 
116
130
  def test_lexical_search_language(self, backend, sample_chunks):
117
131
  """Test lexical search finds language-related content."""
118
132
  backend.store_chunks_batch(sample_chunks)
119
133
 
120
134
  results = backend.search_lexical("languages", k=5)
121
- assert len(results) > 0
135
+
136
+ # Respect the k parameter
137
+ assert 1 <= len(results) <= 5
138
+
122
139
  # Should find the Language class
123
140
  symbols = [r.chunk.symbol for r in results]
124
141
  assert "Language" in symbols
125
142
 
143
+ # Verify scores are descending
144
+ scores = [r.score for r in results]
145
+ assert scores == sorted(scores, reverse=True)
146
+
126
147
 
127
148
  class TestChunkModel:
128
149
  """Test Chunk model validation."""
File without changes
File without changes
File without changes