codespine 1.0.4__tar.gz → 1.0.6__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 (74) hide show
  1. {codespine-1.0.4 → codespine-1.0.6}/PKG-INFO +1 -1
  2. {codespine-1.0.4 → codespine-1.0.6}/codespine/__init__.py +1 -1
  3. {codespine-1.0.4 → codespine-1.0.6}/codespine/cli.py +90 -19
  4. codespine-1.0.6/codespine/db/_cypher_compat.py +523 -0
  5. {codespine-1.0.4 → codespine-1.0.6}/codespine/indexer/call_resolver.py +11 -0
  6. {codespine-1.0.4 → codespine-1.0.6}/codespine/indexer/engine.py +44 -8
  7. {codespine-1.0.4 → codespine-1.0.6}/codespine/sharding/store.py +9 -0
  8. {codespine-1.0.4 → codespine-1.0.6}/codespine.egg-info/PKG-INFO +1 -1
  9. {codespine-1.0.4 → codespine-1.0.6}/pyproject.toml +1 -1
  10. {codespine-1.0.4 → codespine-1.0.6}/tests/test_cypher_compat.py +213 -0
  11. codespine-1.0.4/codespine/db/_cypher_compat.py +0 -309
  12. {codespine-1.0.4 → codespine-1.0.6}/LICENSE +0 -0
  13. {codespine-1.0.4 → codespine-1.0.6}/README.md +0 -0
  14. {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/__init__.py +0 -0
  15. {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/community.py +0 -0
  16. {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/context.py +0 -0
  17. {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/coupling.py +0 -0
  18. {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/crossmodule.py +0 -0
  19. {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/deadcode.py +0 -0
  20. {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/flow.py +0 -0
  21. {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/impact.py +0 -0
  22. {codespine-1.0.4 → codespine-1.0.6}/codespine/cache/__init__.py +0 -0
  23. {codespine-1.0.4 → codespine-1.0.6}/codespine/cache/result_cache.py +0 -0
  24. {codespine-1.0.4 → codespine-1.0.6}/codespine/config.py +0 -0
  25. {codespine-1.0.4 → codespine-1.0.6}/codespine/db/__init__.py +0 -0
  26. {codespine-1.0.4 → codespine-1.0.6}/codespine/db/duckdb_store.py +0 -0
  27. {codespine-1.0.4 → codespine-1.0.6}/codespine/db/schema.py +0 -0
  28. {codespine-1.0.4 → codespine-1.0.6}/codespine/db/store.py +0 -0
  29. {codespine-1.0.4 → codespine-1.0.6}/codespine/diff/__init__.py +0 -0
  30. {codespine-1.0.4 → codespine-1.0.6}/codespine/diff/branch_diff.py +0 -0
  31. {codespine-1.0.4 → codespine-1.0.6}/codespine/guide.py +0 -0
  32. {codespine-1.0.4 → codespine-1.0.6}/codespine/indexer/__init__.py +0 -0
  33. {codespine-1.0.4 → codespine-1.0.6}/codespine/indexer/di_resolver.py +0 -0
  34. {codespine-1.0.4 → codespine-1.0.6}/codespine/indexer/java_parser.py +0 -0
  35. {codespine-1.0.4 → codespine-1.0.6}/codespine/indexer/symbol_builder.py +0 -0
  36. {codespine-1.0.4 → codespine-1.0.6}/codespine/mcp/__init__.py +0 -0
  37. {codespine-1.0.4 → codespine-1.0.6}/codespine/mcp/server.py +0 -0
  38. {codespine-1.0.4 → codespine-1.0.6}/codespine/noise/__init__.py +0 -0
  39. {codespine-1.0.4 → codespine-1.0.6}/codespine/noise/blocklist.py +0 -0
  40. {codespine-1.0.4 → codespine-1.0.6}/codespine/overlay/__init__.py +0 -0
  41. {codespine-1.0.4 → codespine-1.0.6}/codespine/overlay/git_state.py +0 -0
  42. {codespine-1.0.4 → codespine-1.0.6}/codespine/overlay/merge.py +0 -0
  43. {codespine-1.0.4 → codespine-1.0.6}/codespine/overlay/store.py +0 -0
  44. {codespine-1.0.4 → codespine-1.0.6}/codespine/search/__init__.py +0 -0
  45. {codespine-1.0.4 → codespine-1.0.6}/codespine/search/bm25.py +0 -0
  46. {codespine-1.0.4 → codespine-1.0.6}/codespine/search/fuzzy.py +0 -0
  47. {codespine-1.0.4 → codespine-1.0.6}/codespine/search/hybrid.py +0 -0
  48. {codespine-1.0.4 → codespine-1.0.6}/codespine/search/rrf.py +0 -0
  49. {codespine-1.0.4 → codespine-1.0.6}/codespine/search/vector.py +0 -0
  50. {codespine-1.0.4 → codespine-1.0.6}/codespine/sharding/__init__.py +0 -0
  51. {codespine-1.0.4 → codespine-1.0.6}/codespine/sharding/router.py +0 -0
  52. {codespine-1.0.4 → codespine-1.0.6}/codespine/watch/__init__.py +0 -0
  53. {codespine-1.0.4 → codespine-1.0.6}/codespine/watch/git_hook.py +0 -0
  54. {codespine-1.0.4 → codespine-1.0.6}/codespine/watch/watcher.py +0 -0
  55. {codespine-1.0.4 → codespine-1.0.6}/codespine.egg-info/SOURCES.txt +0 -0
  56. {codespine-1.0.4 → codespine-1.0.6}/codespine.egg-info/dependency_links.txt +0 -0
  57. {codespine-1.0.4 → codespine-1.0.6}/codespine.egg-info/entry_points.txt +0 -0
  58. {codespine-1.0.4 → codespine-1.0.6}/codespine.egg-info/requires.txt +0 -0
  59. {codespine-1.0.4 → codespine-1.0.6}/codespine.egg-info/top_level.txt +0 -0
  60. {codespine-1.0.4 → codespine-1.0.6}/gindex.py +0 -0
  61. {codespine-1.0.4 → codespine-1.0.6}/setup.cfg +0 -0
  62. {codespine-1.0.4 → codespine-1.0.6}/tests/test_branch_diff_normalize.py +0 -0
  63. {codespine-1.0.4 → codespine-1.0.6}/tests/test_call_resolver.py +0 -0
  64. {codespine-1.0.4 → codespine-1.0.6}/tests/test_community_detection.py +0 -0
  65. {codespine-1.0.4 → codespine-1.0.6}/tests/test_deadcode.py +0 -0
  66. {codespine-1.0.4 → codespine-1.0.6}/tests/test_duckdb_store.py +0 -0
  67. {codespine-1.0.4 → codespine-1.0.6}/tests/test_index_and_hybrid.py +0 -0
  68. {codespine-1.0.4 → codespine-1.0.6}/tests/test_java_parser.py +0 -0
  69. {codespine-1.0.4 → codespine-1.0.6}/tests/test_multimodule_index.py +0 -0
  70. {codespine-1.0.4 → codespine-1.0.6}/tests/test_overlay.py +0 -0
  71. {codespine-1.0.4 → codespine-1.0.6}/tests/test_result_cache.py +0 -0
  72. {codespine-1.0.4 → codespine-1.0.6}/tests/test_search_ranking.py +0 -0
  73. {codespine-1.0.4 → codespine-1.0.6}/tests/test_sharding.py +0 -0
  74. {codespine-1.0.4 → codespine-1.0.6}/tests/test_store_recovery.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codespine
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: Local Java code intelligence indexer backed by a graph database
5
5
  Author: CodeSpine contributors
6
6
  License: MIT License
@@ -1,4 +1,4 @@
1
1
  """CodeSpine package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "1.0.4"
4
+ __version__ = "1.0.6"
@@ -192,6 +192,21 @@ def _index_shard_group(
192
192
  with output_lock:
193
193
  _phase(f"{prefix}Tracing calls...", "starting...")
194
194
  return
195
+ if event == "resolve_calls_heartbeat":
196
+ # Fires every 2 s from a daemon thread so the spinner stays
197
+ # alive even when the resolver produces no new edges.
198
+ scanned = int(payload.get("scanned", 0))
199
+ edges = int(payload.get("edges", 0))
200
+ elapsed_s = float(payload.get("elapsed", 0.0))
201
+ if not parallel:
202
+ click.echo(
203
+ f"\r{_spinner_char()} {prefix}Tracing calls... "
204
+ f"{edges:>6} resolved / {scanned} scanned {elapsed_s:.1f}s ",
205
+ nl=False,
206
+ )
207
+ call_state["shown"] = True
208
+ call_state["last_ts"] = now
209
+ return
195
210
  if event == "resolve_calls_progress":
196
211
  call_state["count"] = int(payload.get("calls_resolved", 0))
197
212
  if (now - call_state["last_ts"]) >= 0.25:
@@ -345,6 +360,37 @@ def analyse(path: str, full: bool, deep: bool, incremental_deep: bool, embed: bo
345
360
  # For single-project analysis this is transparent — shard() always
346
361
  # returns a GraphStore pointing to the correct shard path.
347
362
  sg = ShardedGraphStore(read_only=False)
363
+
364
+ # ── SIGINT handler: flush partial index on Ctrl+C ────────────────────
365
+ # The handler captures `sg` by closure. On interrupt it snapshots all
366
+ # open shards so `codespine stats` and MCP see the partial result, then
367
+ # calls os._exit(130) to bypass Python cleanup (safe for CLI process).
368
+ # A second Ctrl+C hard-exits immediately.
369
+ _sigint_pressed: list[bool] = [False]
370
+ _old_sigint_handler = signal.getsignal(signal.SIGINT)
371
+
372
+ def _sigint_flush(signum: int, frame: object) -> None: # noqa: ARG001
373
+ if _sigint_pressed[0]:
374
+ os._exit(130)
375
+ _sigint_pressed[0] = True
376
+ # Restore default handler so a second Ctrl+C exits immediately.
377
+ signal.signal(signal.SIGINT, signal.default_int_handler)
378
+ click.secho(
379
+ "\n\n⚠ Interrupted — flushing partial index to read replica…",
380
+ fg="yellow",
381
+ )
382
+ try:
383
+ sg.snapshot_all(background=False)
384
+ click.secho(
385
+ "✓ Partial index saved. Run 'codespine stats' to see what was indexed.",
386
+ fg="yellow",
387
+ )
388
+ except Exception: # noqa: BLE001
389
+ pass
390
+ os._exit(130)
391
+
392
+ signal.signal(signal.SIGINT, _sigint_flush)
393
+
348
394
  # The indexer is initialised per-module below with the right shard store.
349
395
  # We keep a single ShardedGraphStore to fan-out cross-module linking later.
350
396
 
@@ -537,21 +583,28 @@ def analyse(path: str, full: bool, deep: bool, incremental_deep: bool, embed: bo
537
583
 
538
584
  _phase("Analyzing git history...", "skipped (large repo; rerun with --deep)")
539
585
 
540
- vector_count = root_shard_store.query_records(
586
+ # Summary queries are best-effort: a translator miss or a transient
587
+ # DB error must never throw away a successful index.
588
+ def _safe_count(query: str) -> int:
589
+ try:
590
+ rows = root_shard_store.query_records(query)
591
+ return int(rows[0]["count"]) if rows else 0
592
+ except Exception as exc: # noqa: BLE001 - summary stats are non-critical
593
+ click.secho(f" (summary stat unavailable: {exc})", fg="yellow")
594
+ return 0
595
+
596
+ embeddings_generated = last_result.embeddings_generated if last_result else 0
597
+ vectors_stored = _safe_count(
541
598
  """
542
599
  MATCH (s:Symbol)
543
600
  WHERE s.embedding IS NOT NULL
544
601
  RETURN count(s) as count
545
602
  """
546
- )
547
- embeddings_generated = last_result.embeddings_generated if last_result else 0
548
- vectors_stored = int(vector_count[0]["count"]) if vector_count else embeddings_generated
603
+ ) or embeddings_generated
549
604
  _phase("Generating embeddings...", f"{vectors_stored} vectors stored")
550
605
 
551
- symbol_count = root_shard_store.query_records("MATCH (s:Symbol) RETURN count(s) as count")
552
- edge_count = root_shard_store.query_records("MATCH ()-[r]->() RETURN count(r) as count")
553
- symbols = int(symbol_count[0]["count"]) if symbol_count else 0
554
- edges = int(edge_count[0]["count"]) if edge_count else 0
606
+ symbols = _safe_count("MATCH (s:Symbol) RETURN count(s) as count")
607
+ edges = _safe_count("MATCH ()-[r]->() RETURN count(r) as count")
555
608
  elapsed = time.perf_counter() - started
556
609
 
557
610
  if not embed:
@@ -587,6 +640,9 @@ def analyse(path: str, full: bool, deep: bool, incremental_deep: bool, embed: bo
587
640
  sg.snapshot_all(background=False)
588
641
  _finish_phase(snap_label, "MCP will reload automatically")
589
642
 
643
+ # Restore original SIGINT handler now that we've finished cleanly.
644
+ signal.signal(signal.SIGINT, _old_sigint_handler)
645
+
590
646
 
591
647
  @main.command()
592
648
  @click.argument("query")
@@ -734,15 +790,27 @@ def stats(as_json: bool, show_shards: bool) -> None:
734
790
  click.secho("No projects indexed yet. Run 'codespine analyse <path>'.", fg="yellow")
735
791
  return
736
792
 
793
+ def _stat_count(store, query: str, params: dict) -> int:
794
+ """Run a stats count query — returns 0 on any failure."""
795
+ try:
796
+ rows = store.query_records(query, params)
797
+ return int(rows[0]["n"]) if rows else 0
798
+ except Exception as exc: # noqa: BLE001
799
+ click.secho(f" (stat unavailable: {exc})", fg="yellow")
800
+ return 0
801
+
737
802
  rows = []
738
803
  for p in all_projects_meta:
739
804
  pid = p["id"]
740
805
  # Route each query to the project's owning shard.
741
806
  ps = _project_store(pid)
742
- files = ps.query_records(
743
- "MATCH (f:File) WHERE f.project_id = $pid RETURN count(f) as n", {"pid": pid}
807
+ n_files = _stat_count(
808
+ ps,
809
+ "MATCH (f:File) WHERE f.project_id = $pid RETURN count(f) as n",
810
+ {"pid": pid},
744
811
  )
745
- classes = ps.query_records(
812
+ n_classes = _stat_count(
813
+ ps,
746
814
  """
747
815
  MATCH (f:File) WHERE f.project_id = $pid
748
816
  WITH f
@@ -751,7 +819,8 @@ def stats(as_json: bool, show_shards: bool) -> None:
751
819
  """,
752
820
  {"pid": pid},
753
821
  )
754
- methods = ps.query_records(
822
+ n_methods = _stat_count(
823
+ ps,
755
824
  """
756
825
  MATCH (f:File) WHERE f.project_id = $pid
757
826
  WITH f
@@ -762,7 +831,8 @@ def stats(as_json: bool, show_shards: bool) -> None:
762
831
  """,
763
832
  {"pid": pid},
764
833
  )
765
- calls = ps.query_records(
834
+ n_calls = _stat_count(
835
+ ps,
766
836
  """
767
837
  MATCH (f:File) WHERE f.project_id = $pid
768
838
  WITH f
@@ -773,7 +843,8 @@ def stats(as_json: bool, show_shards: bool) -> None:
773
843
  """,
774
844
  {"pid": pid},
775
845
  )
776
- emb = ps.query_records(
846
+ n_emb = _stat_count(
847
+ ps,
777
848
  """
778
849
  MATCH (f:File) WHERE f.project_id = $pid
779
850
  WITH f
@@ -786,11 +857,11 @@ def stats(as_json: bool, show_shards: bool) -> None:
786
857
  "project": pid,
787
858
  "path": p["path"],
788
859
  "shard": sg.router.shard_for(pid),
789
- "files": files[0]["n"] if files else 0,
790
- "classes": classes[0]["n"] if classes else 0,
791
- "methods": methods[0]["n"] if methods else 0,
792
- "calls_out": calls[0]["n"] if calls else 0,
793
- "embeddings": emb[0]["n"] if emb else 0,
860
+ "files": n_files,
861
+ "classes": n_classes,
862
+ "methods": n_methods,
863
+ "calls_out": n_calls,
864
+ "embeddings": n_emb,
794
865
  })
795
866
 
796
867
  if as_json: