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.
- {codespine-1.0.4 → codespine-1.0.6}/PKG-INFO +1 -1
- {codespine-1.0.4 → codespine-1.0.6}/codespine/__init__.py +1 -1
- {codespine-1.0.4 → codespine-1.0.6}/codespine/cli.py +90 -19
- codespine-1.0.6/codespine/db/_cypher_compat.py +523 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/indexer/call_resolver.py +11 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/indexer/engine.py +44 -8
- {codespine-1.0.4 → codespine-1.0.6}/codespine/sharding/store.py +9 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine.egg-info/PKG-INFO +1 -1
- {codespine-1.0.4 → codespine-1.0.6}/pyproject.toml +1 -1
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_cypher_compat.py +213 -0
- codespine-1.0.4/codespine/db/_cypher_compat.py +0 -309
- {codespine-1.0.4 → codespine-1.0.6}/LICENSE +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/README.md +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/community.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/context.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/coupling.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/crossmodule.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/deadcode.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/flow.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/analysis/impact.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/cache/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/cache/result_cache.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/config.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/db/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/db/duckdb_store.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/db/schema.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/db/store.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/diff/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/diff/branch_diff.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/guide.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/indexer/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/indexer/di_resolver.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/indexer/java_parser.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/indexer/symbol_builder.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/mcp/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/mcp/server.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/noise/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/noise/blocklist.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/overlay/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/overlay/git_state.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/overlay/merge.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/overlay/store.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/search/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/search/bm25.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/search/fuzzy.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/search/hybrid.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/search/rrf.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/search/vector.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/sharding/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/sharding/router.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/watch/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/watch/git_hook.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine/watch/watcher.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine.egg-info/SOURCES.txt +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine.egg-info/dependency_links.txt +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine.egg-info/entry_points.txt +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine.egg-info/requires.txt +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/codespine.egg-info/top_level.txt +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/gindex.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/setup.cfg +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_branch_diff_normalize.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_call_resolver.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_community_detection.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_deadcode.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_duckdb_store.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_index_and_hybrid.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_java_parser.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_multimodule_index.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_overlay.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_result_cache.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_search_ranking.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_sharding.py +0 -0
- {codespine-1.0.4 → codespine-1.0.6}/tests/test_store_recovery.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
743
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":
|
|
790
|
-
"classes":
|
|
791
|
-
"methods":
|
|
792
|
-
"calls_out":
|
|
793
|
-
"embeddings":
|
|
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:
|