codespine 1.0.10__tar.gz → 1.0.11__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 (78) hide show
  1. {codespine-1.0.10 → codespine-1.0.11}/PKG-INFO +5 -4
  2. {codespine-1.0.10 → codespine-1.0.11}/README.md +4 -3
  3. {codespine-1.0.10 → codespine-1.0.11}/codespine/__init__.py +1 -1
  4. {codespine-1.0.10 → codespine-1.0.11}/codespine/cli.py +101 -3
  5. {codespine-1.0.10 → codespine-1.0.11}/codespine/config.py +3 -3
  6. {codespine-1.0.10 → codespine-1.0.11}/codespine/indexer/engine.py +40 -18
  7. {codespine-1.0.10 → codespine-1.0.11}/codespine.egg-info/PKG-INFO +5 -4
  8. {codespine-1.0.10 → codespine-1.0.11}/pyproject.toml +1 -1
  9. {codespine-1.0.10 → codespine-1.0.11}/tests/test_tasks.py +12 -0
  10. {codespine-1.0.10 → codespine-1.0.11}/LICENSE +0 -0
  11. {codespine-1.0.10 → codespine-1.0.11}/codespine/analysis/__init__.py +0 -0
  12. {codespine-1.0.10 → codespine-1.0.11}/codespine/analysis/community.py +0 -0
  13. {codespine-1.0.10 → codespine-1.0.11}/codespine/analysis/context.py +0 -0
  14. {codespine-1.0.10 → codespine-1.0.11}/codespine/analysis/coupling.py +0 -0
  15. {codespine-1.0.10 → codespine-1.0.11}/codespine/analysis/crossmodule.py +0 -0
  16. {codespine-1.0.10 → codespine-1.0.11}/codespine/analysis/deadcode.py +0 -0
  17. {codespine-1.0.10 → codespine-1.0.11}/codespine/analysis/flow.py +0 -0
  18. {codespine-1.0.10 → codespine-1.0.11}/codespine/analysis/impact.py +0 -0
  19. {codespine-1.0.10 → codespine-1.0.11}/codespine/cache/__init__.py +0 -0
  20. {codespine-1.0.10 → codespine-1.0.11}/codespine/cache/result_cache.py +0 -0
  21. {codespine-1.0.10 → codespine-1.0.11}/codespine/db/__init__.py +0 -0
  22. {codespine-1.0.10 → codespine-1.0.11}/codespine/db/_cypher_compat.py +0 -0
  23. {codespine-1.0.10 → codespine-1.0.11}/codespine/db/duckdb_store.py +0 -0
  24. {codespine-1.0.10 → codespine-1.0.11}/codespine/db/schema.py +0 -0
  25. {codespine-1.0.10 → codespine-1.0.11}/codespine/db/store.py +0 -0
  26. {codespine-1.0.10 → codespine-1.0.11}/codespine/diff/__init__.py +0 -0
  27. {codespine-1.0.10 → codespine-1.0.11}/codespine/diff/branch_diff.py +0 -0
  28. {codespine-1.0.10 → codespine-1.0.11}/codespine/guide.py +0 -0
  29. {codespine-1.0.10 → codespine-1.0.11}/codespine/health.py +0 -0
  30. {codespine-1.0.10 → codespine-1.0.11}/codespine/indexer/__init__.py +0 -0
  31. {codespine-1.0.10 → codespine-1.0.11}/codespine/indexer/call_resolver.py +0 -0
  32. {codespine-1.0.10 → codespine-1.0.11}/codespine/indexer/di_resolver.py +0 -0
  33. {codespine-1.0.10 → codespine-1.0.11}/codespine/indexer/java_parser.py +0 -0
  34. {codespine-1.0.10 → codespine-1.0.11}/codespine/indexer/symbol_builder.py +0 -0
  35. {codespine-1.0.10 → codespine-1.0.11}/codespine/mcp/__init__.py +0 -0
  36. {codespine-1.0.10 → codespine-1.0.11}/codespine/mcp/server.py +0 -0
  37. {codespine-1.0.10 → codespine-1.0.11}/codespine/noise/__init__.py +0 -0
  38. {codespine-1.0.10 → codespine-1.0.11}/codespine/noise/blocklist.py +0 -0
  39. {codespine-1.0.10 → codespine-1.0.11}/codespine/overlay/__init__.py +0 -0
  40. {codespine-1.0.10 → codespine-1.0.11}/codespine/overlay/git_state.py +0 -0
  41. {codespine-1.0.10 → codespine-1.0.11}/codespine/overlay/merge.py +0 -0
  42. {codespine-1.0.10 → codespine-1.0.11}/codespine/overlay/store.py +0 -0
  43. {codespine-1.0.10 → codespine-1.0.11}/codespine/search/__init__.py +0 -0
  44. {codespine-1.0.10 → codespine-1.0.11}/codespine/search/bm25.py +0 -0
  45. {codespine-1.0.10 → codespine-1.0.11}/codespine/search/fuzzy.py +0 -0
  46. {codespine-1.0.10 → codespine-1.0.11}/codespine/search/hybrid.py +0 -0
  47. {codespine-1.0.10 → codespine-1.0.11}/codespine/search/rrf.py +0 -0
  48. {codespine-1.0.10 → codespine-1.0.11}/codespine/search/vector.py +0 -0
  49. {codespine-1.0.10 → codespine-1.0.11}/codespine/sharding/__init__.py +0 -0
  50. {codespine-1.0.10 → codespine-1.0.11}/codespine/sharding/router.py +0 -0
  51. {codespine-1.0.10 → codespine-1.0.11}/codespine/sharding/store.py +0 -0
  52. {codespine-1.0.10 → codespine-1.0.11}/codespine/tasks.py +0 -0
  53. {codespine-1.0.10 → codespine-1.0.11}/codespine/watch/__init__.py +0 -0
  54. {codespine-1.0.10 → codespine-1.0.11}/codespine/watch/git_hook.py +0 -0
  55. {codespine-1.0.10 → codespine-1.0.11}/codespine/watch/watcher.py +0 -0
  56. {codespine-1.0.10 → codespine-1.0.11}/codespine.egg-info/SOURCES.txt +0 -0
  57. {codespine-1.0.10 → codespine-1.0.11}/codespine.egg-info/dependency_links.txt +0 -0
  58. {codespine-1.0.10 → codespine-1.0.11}/codespine.egg-info/entry_points.txt +0 -0
  59. {codespine-1.0.10 → codespine-1.0.11}/codespine.egg-info/requires.txt +0 -0
  60. {codespine-1.0.10 → codespine-1.0.11}/codespine.egg-info/top_level.txt +0 -0
  61. {codespine-1.0.10 → codespine-1.0.11}/gindex.py +0 -0
  62. {codespine-1.0.10 → codespine-1.0.11}/setup.cfg +0 -0
  63. {codespine-1.0.10 → codespine-1.0.11}/tests/test_branch_diff_normalize.py +0 -0
  64. {codespine-1.0.10 → codespine-1.0.11}/tests/test_call_resolver.py +0 -0
  65. {codespine-1.0.10 → codespine-1.0.11}/tests/test_community_detection.py +0 -0
  66. {codespine-1.0.10 → codespine-1.0.11}/tests/test_cypher_compat.py +0 -0
  67. {codespine-1.0.10 → codespine-1.0.11}/tests/test_deadcode.py +0 -0
  68. {codespine-1.0.10 → codespine-1.0.11}/tests/test_duckdb_store.py +0 -0
  69. {codespine-1.0.10 → codespine-1.0.11}/tests/test_health.py +0 -0
  70. {codespine-1.0.10 → codespine-1.0.11}/tests/test_index_and_hybrid.py +0 -0
  71. {codespine-1.0.10 → codespine-1.0.11}/tests/test_java_parser.py +0 -0
  72. {codespine-1.0.10 → codespine-1.0.11}/tests/test_multimodule_index.py +0 -0
  73. {codespine-1.0.10 → codespine-1.0.11}/tests/test_overlay.py +0 -0
  74. {codespine-1.0.10 → codespine-1.0.11}/tests/test_parse_resilience.py +0 -0
  75. {codespine-1.0.10 → codespine-1.0.11}/tests/test_result_cache.py +0 -0
  76. {codespine-1.0.10 → codespine-1.0.11}/tests/test_search_ranking.py +0 -0
  77. {codespine-1.0.10 → codespine-1.0.11}/tests/test_sharding.py +0 -0
  78. {codespine-1.0.10 → codespine-1.0.11}/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.10
3
+ Version: 1.0.11
4
4
  Summary: Local Java code intelligence indexer backed by a graph database
5
5
  Author: CodeSpine contributors
6
6
  License: MIT License
@@ -66,7 +66,7 @@ Dynamic: license-file
66
66
 
67
67
  # CodeSpine
68
68
 
69
- **v1.0.10** — Local Java code intelligence for coding agents, backed by a graph database.
69
+ **v1.0.11** — Local Java code intelligence for coding agents, backed by a graph database.
70
70
 
71
71
  CodeSpine cuts token burn for coding agents working on Java codebases.
72
72
 
@@ -111,6 +111,7 @@ pip install "codespine[ui]"
111
111
  ```
112
112
 
113
113
  The current lite UI is dependency-free and served locally by CodeSpine; the `ui` extra is the stable add-on install target for the browser explorer.
114
+ Quotes are required in zsh: use `pip install "codespine[ui]"`, not `pip install codespine[ui]`.
114
115
 
115
116
  Everything at once (ml + community detection):
116
117
 
@@ -381,7 +382,7 @@ codespine force-reset # emergency: delete all data files
381
382
 
382
383
  `analyse` defaults to incremental mode. Repeat runs only process changed files and are fast.
383
384
 
384
- `analyse` runs in fast mode by default: it indexes the core graph, publishes that read replica from a detached process, then continues communities, flows, dead code, coupling, and cross-module enrichment in the background. Use `codespine tasks` or `codespine ui` to watch that work. Use `--complete --deep` when you want those passes refreshed before the command returns.
385
+ `analyse` runs in fast mode by default: it indexes the core graph, honors the foreground time budget, publishes a read replica, then continues unfinished core indexing or enrichment in the background. Use `codespine background` or `codespine ui` to watch that work. Use `--complete --deep` when you want those passes refreshed before the command returns.
385
386
 
386
387
  ---
387
388
 
@@ -567,7 +568,7 @@ The deep analysis phase covers four passes that are expensive but optional:
567
568
  | Dead code | Finds methods with no callers (Java-aware exemptions) | Cleanup audits |
568
569
  | Change coupling | Analyses git history for co-changed file pairs | `get_change_coupling`, `related` |
569
570
 
570
- **Fast default:** `codespine analyse` prioritizes a queryable core index. Communities, flows, dead-code, git coupling, and cross-module links are queued in a detached background enrichment job unless you use `--complete`.
571
+ **Fast default:** `codespine analyse` prioritizes a queryable core index and keeps the foreground run inside the configured budget. If core indexing or call/type resolution cannot finish in time, the partial index is published and a background continuation finishes the core graph. Communities, flows, dead-code, git coupling, and cross-module links are queued in a detached background enrichment job unless you use `--complete`.
571
572
 
572
573
  **Health checks:** every analyse run now performs a small self-test query suite and reports index anomalies such as large projects with zero call edges. Use `codespine health` for the terminal dashboard or `codespine self-test --json` in CI.
573
574
 
@@ -1,6 +1,6 @@
1
1
  # CodeSpine
2
2
 
3
- **v1.0.10** — Local Java code intelligence for coding agents, backed by a graph database.
3
+ **v1.0.11** — Local Java code intelligence for coding agents, backed by a graph database.
4
4
 
5
5
  CodeSpine cuts token burn for coding agents working on Java codebases.
6
6
 
@@ -45,6 +45,7 @@ pip install "codespine[ui]"
45
45
  ```
46
46
 
47
47
  The current lite UI is dependency-free and served locally by CodeSpine; the `ui` extra is the stable add-on install target for the browser explorer.
48
+ Quotes are required in zsh: use `pip install "codespine[ui]"`, not `pip install codespine[ui]`.
48
49
 
49
50
  Everything at once (ml + community detection):
50
51
 
@@ -315,7 +316,7 @@ codespine force-reset # emergency: delete all data files
315
316
 
316
317
  `analyse` defaults to incremental mode. Repeat runs only process changed files and are fast.
317
318
 
318
- `analyse` runs in fast mode by default: it indexes the core graph, publishes that read replica from a detached process, then continues communities, flows, dead code, coupling, and cross-module enrichment in the background. Use `codespine tasks` or `codespine ui` to watch that work. Use `--complete --deep` when you want those passes refreshed before the command returns.
319
+ `analyse` runs in fast mode by default: it indexes the core graph, honors the foreground time budget, publishes a read replica, then continues unfinished core indexing or enrichment in the background. Use `codespine background` or `codespine ui` to watch that work. Use `--complete --deep` when you want those passes refreshed before the command returns.
319
320
 
320
321
  ---
321
322
 
@@ -501,7 +502,7 @@ The deep analysis phase covers four passes that are expensive but optional:
501
502
  | Dead code | Finds methods with no callers (Java-aware exemptions) | Cleanup audits |
502
503
  | Change coupling | Analyses git history for co-changed file pairs | `get_change_coupling`, `related` |
503
504
 
504
- **Fast default:** `codespine analyse` prioritizes a queryable core index. Communities, flows, dead-code, git coupling, and cross-module links are queued in a detached background enrichment job unless you use `--complete`.
505
+ **Fast default:** `codespine analyse` prioritizes a queryable core index and keeps the foreground run inside the configured budget. If core indexing or call/type resolution cannot finish in time, the partial index is published and a background continuation finishes the core graph. Communities, flows, dead-code, git coupling, and cross-module links are queued in a detached background enrichment job unless you use `--complete`.
505
506
 
506
507
  **Health checks:** every analyse run now performs a small self-test query suite and reports index anomalies such as large projects with zero call edges. Use `codespine health` for the terminal dashboard or `codespine self-test --json` in CI.
507
508
 
@@ -1,4 +1,4 @@
1
1
  """CodeSpine package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "1.0.10"
4
+ __version__ = "1.0.11"
@@ -98,6 +98,32 @@ def _spawn_background_enrichment(path: str) -> bool:
98
98
  return False
99
99
 
100
100
 
101
+ def _spawn_background_continuation(path: str) -> bool:
102
+ """Continue a budget-paused core index in a detached process."""
103
+ task_id = create_task(
104
+ "indexing",
105
+ "Background core indexing",
106
+ path=path,
107
+ metadata={"trigger": "analyse-budget"},
108
+ )
109
+ try:
110
+ proc = subprocess.Popen(
111
+ [sys.executable, "-m", "codespine.cli", "continue-background", path, "--task-id", task_id],
112
+ stdin=subprocess.DEVNULL,
113
+ stdout=subprocess.DEVNULL,
114
+ stderr=subprocess.DEVNULL,
115
+ start_new_session=True,
116
+ cwd=os.getcwd(),
117
+ env=os.environ.copy(),
118
+ )
119
+ update_task(task_id, status="running", phase="spawned", pid=proc.pid)
120
+ return True
121
+ except Exception as exc: # noqa: BLE001
122
+ finish_task(task_id, "failed", str(exc))
123
+ LOGGER.warning("Unable to spawn background continuation: %s", exc)
124
+ return False
125
+
126
+
101
127
  def _db_size_bytes(path: str) -> int:
102
128
  if os.path.isfile(path):
103
129
  return os.path.getsize(path)
@@ -373,6 +399,16 @@ def _index_shard_group(
373
399
  f"{files} files, {classes} classes, {methods} methods ({elapsed_s:.1f}s)",
374
400
  )
375
401
  return
402
+ if event == "budget_exhausted":
403
+ files = int(payload.get("files_indexed", 0))
404
+ total = int(payload.get("total", 0))
405
+ phase = str(payload.get("phase", "indexing"))
406
+ with output_lock:
407
+ _phase(
408
+ f"{prefix}Foreground budget...",
409
+ f"paused during {phase} ({files}/{total} files); continuing in background",
410
+ )
411
+ return
376
412
  if event in ("resolve_calls_start",):
377
413
  if parse_state["shown"] or db_state["shown"]:
378
414
  with output_lock:
@@ -906,7 +942,16 @@ def analyse(
906
942
  recycle = getattr(store, "_recycle_conn", None)
907
943
  if callable(recycle):
908
944
  recycle()
909
- if fast and _spawn_background_enrichment(abs_path):
945
+ core_partial = any(bool(getattr(result, "partial", False)) for result in all_results)
946
+ if fast and core_partial:
947
+ _live_phase(snap_label, "copying partial core")
948
+ sg.snapshot_all(background=False)
949
+ _finish_phase(snap_label, "partial index visible")
950
+ if _spawn_background_continuation(abs_path):
951
+ _phase("Background indexing...", "core indexing continues; run 'codespine background'")
952
+ else:
953
+ _phase("Background indexing...", "not started; rerun 'codespine analyse' to continue")
954
+ elif fast and _spawn_background_enrichment(abs_path):
910
955
  _phase(snap_label, "core snapshot now; enrichment continues in background")
911
956
  else:
912
957
  _live_phase(snap_label, "copying")
@@ -1023,6 +1068,50 @@ def enrich_background(path: str, task_id: str | None) -> None:
1023
1068
  raise
1024
1069
 
1025
1070
 
1071
+ @main.command("continue-background", hidden=True)
1072
+ @click.argument("path", type=click.Path(exists=True))
1073
+ @click.option("--task-id", default=None, hidden=True)
1074
+ def continue_background(path: str, task_id: str | None) -> None:
1075
+ """Continue core indexing after a foreground budget pause."""
1076
+ abs_path = os.path.abspath(path)
1077
+ if task_id is None:
1078
+ task_id = create_task("indexing", "Background core indexing", path=abs_path)
1079
+ update_task(
1080
+ task_id,
1081
+ status="running",
1082
+ phase="core indexing",
1083
+ pid=os.getpid(),
1084
+ progress=0.10,
1085
+ detail="Continuing analyse without the foreground budget",
1086
+ )
1087
+ try:
1088
+ with open(SETTINGS.log_file, "a", encoding="utf-8") as log:
1089
+ proc = subprocess.run(
1090
+ [
1091
+ sys.executable,
1092
+ "-m",
1093
+ "codespine.cli",
1094
+ "analyse",
1095
+ abs_path,
1096
+ "--budget",
1097
+ "0",
1098
+ "--allow-running",
1099
+ ],
1100
+ cwd=os.getcwd(),
1101
+ stdout=log,
1102
+ stderr=subprocess.STDOUT,
1103
+ check=False,
1104
+ )
1105
+ if proc.returncode == 0:
1106
+ finish_task(task_id, "succeeded", "Background core indexing complete")
1107
+ return
1108
+ finish_task(task_id, "failed", f"Background analyse exited with code {proc.returncode}")
1109
+ raise click.ClickException(f"Background analyse exited with code {proc.returncode}")
1110
+ except Exception as exc: # noqa: BLE001
1111
+ finish_task(task_id, "failed", str(exc))
1112
+ raise
1113
+
1114
+
1026
1115
  @main.command()
1027
1116
  @click.argument("query")
1028
1117
  @click.option("--k", default=20, show_default=True, type=int)
@@ -1363,7 +1452,13 @@ def tasks_cmd(include_finished: bool, limit: int, as_json: bool) -> None:
1363
1452
 
1364
1453
 
1365
1454
  @main.command("background")
1366
- @click.option("--all", "include_finished", is_flag=True, help="Include completed and failed tasks.")
1455
+ @click.option(
1456
+ "--all/--running-only",
1457
+ "include_finished",
1458
+ default=True,
1459
+ show_default=True,
1460
+ help="Include completed and failed tasks, or show only currently running work.",
1461
+ )
1367
1462
  @click.option("--limit", default=20, show_default=True, type=int)
1368
1463
  @click.option("--json", "as_json", is_flag=True)
1369
1464
  def background_cmd(include_finished: bool, limit: int, as_json: bool) -> None:
@@ -1377,7 +1472,10 @@ def _show_background_tasks(include_finished: bool, limit: int, as_json: bool) ->
1377
1472
  _echo_json(tasks, True)
1378
1473
  return
1379
1474
  if not tasks:
1380
- click.echo("No background tasks.")
1475
+ if include_finished:
1476
+ click.echo("No background tasks recorded.")
1477
+ else:
1478
+ click.echo("No running background tasks.")
1381
1479
  return
1382
1480
  now = time.time()
1383
1481
  header = f"{'ID':<12} {'Status':<10} {'Progress':>8} {'Phase':<26} {'Age':>7} Path"
@@ -30,9 +30,9 @@ class Settings:
30
30
  rrf_k: int = 60
31
31
  semantic_candidate_pool: int = 2000
32
32
  write_batch_size: int = 500
33
- index_file_batch_size: int = 200
34
- index_method_batch_size: int = 2000
35
- index_symbol_batch_size: int = 2000
33
+ index_file_batch_size: int = 1000
34
+ index_method_batch_size: int = 10000
35
+ index_symbol_batch_size: int = 10000
36
36
  edge_write_batch_size: int = 5000
37
37
  default_coupling_days: int = 5
38
38
  default_min_coupling_strength: float = 0.3
@@ -88,6 +88,7 @@ class IndexResult:
88
88
  calls_resolved: int
89
89
  type_relationships: int
90
90
  embeddings_generated: int
91
+ partial: bool = False
91
92
 
92
93
 
93
94
  class JavaIndexer:
@@ -429,6 +430,7 @@ class JavaIndexer:
429
430
  total=_db_total,
430
431
  deleted_files=len(deleted_file_ids),
431
432
  )
433
+ budget_exhausted = False
432
434
 
433
435
  try:
434
436
  # ── Chunked DB writes ─────────────────────────────────────────────
@@ -644,26 +646,20 @@ class JavaIndexer:
644
646
  with self.store.transaction():
645
647
  self.store.clear_files_batch(clear_sub)
646
648
  self.store._recycle_conn()
647
- _db_phase_holder[0] = "writing files"
649
+ _db_phase_holder[0] = "writing rows"
648
650
  with self.store.transaction():
649
651
  self.store.upsert_files_batch(file_rows)
650
- self.store._recycle_conn()
651
- _db_phase_holder[0] = "writing classes"
652
- with self.store.transaction():
653
652
  self.store.upsert_classes_batch(class_rows)
654
- self.store._recycle_conn()
655
- _METHOD_SUB_BATCH = max(1, int(getattr(SETTINGS, "index_method_batch_size", 2000)))
656
- _db_phase_holder[0] = "writing methods"
657
- for method_sub in self._chunked(method_rows, _METHOD_SUB_BATCH):
658
- with self.store.transaction():
653
+ _METHOD_SUB_BATCH = max(1, int(getattr(SETTINGS, "index_method_batch_size", 10000)))
654
+ _db_phase_holder[0] = "writing methods"
655
+ for method_sub in self._chunked(method_rows, _METHOD_SUB_BATCH):
659
656
  self.store.upsert_methods_batch(method_sub)
660
- self.store._recycle_conn()
661
- _SYMBOL_SUB_BATCH = max(1, int(getattr(SETTINGS, "index_symbol_batch_size", 2000)))
662
- _db_phase_holder[0] = "writing symbols"
663
- for symbol_sub in self._chunked(symbol_rows, _SYMBOL_SUB_BATCH):
664
- with self.store.transaction():
657
+ _SYMBOL_SUB_BATCH = max(1, int(getattr(SETTINGS, "index_symbol_batch_size", 10000)))
658
+ _db_phase_holder[0] = "writing symbols"
659
+ for symbol_sub in self._chunked(symbol_rows, _SYMBOL_SUB_BATCH):
665
660
  self.store.upsert_symbols_batch(symbol_sub)
666
- self.store._recycle_conn()
661
+ self.store._recycle_conn()
662
+ _db_phase_holder[0] = "building rows"
667
663
  self._emit(
668
664
  progress,
669
665
  "db_write_progress",
@@ -673,6 +669,16 @@ class JavaIndexer:
673
669
  methods=methods_indexed,
674
670
  phase=_db_phase_holder[0],
675
671
  )
672
+ if deadline is not None and time.perf_counter() >= max(0.0, deadline - 8.0):
673
+ budget_exhausted = True
674
+ self._emit(
675
+ progress,
676
+ "budget_exhausted",
677
+ phase="db_write",
678
+ files_indexed=files_indexed,
679
+ total=_db_total,
680
+ )
681
+ break
676
682
  _db_phase_holder[0] = "building rows"
677
683
  finally:
678
684
  _db_hb_stop.set()
@@ -688,7 +694,23 @@ class JavaIndexer:
688
694
  )
689
695
 
690
696
  def _deadline_expired() -> bool:
691
- return deadline is not None and time.perf_counter() >= deadline
697
+ return budget_exhausted or (deadline is not None and time.perf_counter() >= deadline)
698
+
699
+ if budget_exhausted:
700
+ self._emit(progress, "resolve_calls_done", calls_resolved=0, partial=True)
701
+ self._emit(progress, "resolve_types_done", type_relationships=0, partial=True)
702
+ self._emit(progress, "di_done", injections=0, interface_bindings=0, partial=True)
703
+ return IndexResult(
704
+ project_id=project_id,
705
+ files_found=len(current_files),
706
+ files_indexed=files_indexed,
707
+ classes_indexed=classes_indexed,
708
+ methods_indexed=methods_indexed,
709
+ calls_resolved=0,
710
+ type_relationships=0,
711
+ embeddings_generated=classes_indexed + methods_indexed if embed else 0,
712
+ partial=True,
713
+ )
692
714
 
693
715
  self._emit(progress, "resolve_calls_start")
694
716
 
@@ -770,8 +792,6 @@ class JavaIndexer:
770
792
  if _deadline_expired():
771
793
  self._emit(progress, "resolve_types_done", type_relationships=0, partial=True)
772
794
  self._emit(progress, "di_done", injections=0, interface_bindings=0, partial=True)
773
- self._prune_meta_cache(meta_cache, current_file_ids)
774
- self._save_file_meta_cache(project_id, meta_cache)
775
795
  return IndexResult(
776
796
  project_id=project_id,
777
797
  files_found=len(current_files),
@@ -781,6 +801,7 @@ class JavaIndexer:
781
801
  calls_resolved=calls_resolved,
782
802
  type_relationships=0,
783
803
  embeddings_generated=classes_indexed + methods_indexed if embed else 0,
804
+ partial=True,
784
805
  )
785
806
  type_rows = self._build_inheritance_edges(
786
807
  class_meta,
@@ -843,6 +864,7 @@ class JavaIndexer:
843
864
  calls_resolved=calls_resolved,
844
865
  type_relationships=type_relationships,
845
866
  embeddings_generated=classes_indexed + methods_indexed if embed else 0,
867
+ partial=partial_calls,
846
868
  )
847
869
 
848
870
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codespine
3
- Version: 1.0.10
3
+ Version: 1.0.11
4
4
  Summary: Local Java code intelligence indexer backed by a graph database
5
5
  Author: CodeSpine contributors
6
6
  License: MIT License
@@ -66,7 +66,7 @@ Dynamic: license-file
66
66
 
67
67
  # CodeSpine
68
68
 
69
- **v1.0.10** — Local Java code intelligence for coding agents, backed by a graph database.
69
+ **v1.0.11** — Local Java code intelligence for coding agents, backed by a graph database.
70
70
 
71
71
  CodeSpine cuts token burn for coding agents working on Java codebases.
72
72
 
@@ -111,6 +111,7 @@ pip install "codespine[ui]"
111
111
  ```
112
112
 
113
113
  The current lite UI is dependency-free and served locally by CodeSpine; the `ui` extra is the stable add-on install target for the browser explorer.
114
+ Quotes are required in zsh: use `pip install "codespine[ui]"`, not `pip install codespine[ui]`.
114
115
 
115
116
  Everything at once (ml + community detection):
116
117
 
@@ -381,7 +382,7 @@ codespine force-reset # emergency: delete all data files
381
382
 
382
383
  `analyse` defaults to incremental mode. Repeat runs only process changed files and are fast.
383
384
 
384
- `analyse` runs in fast mode by default: it indexes the core graph, publishes that read replica from a detached process, then continues communities, flows, dead code, coupling, and cross-module enrichment in the background. Use `codespine tasks` or `codespine ui` to watch that work. Use `--complete --deep` when you want those passes refreshed before the command returns.
385
+ `analyse` runs in fast mode by default: it indexes the core graph, honors the foreground time budget, publishes a read replica, then continues unfinished core indexing or enrichment in the background. Use `codespine background` or `codespine ui` to watch that work. Use `--complete --deep` when you want those passes refreshed before the command returns.
385
386
 
386
387
  ---
387
388
 
@@ -567,7 +568,7 @@ The deep analysis phase covers four passes that are expensive but optional:
567
568
  | Dead code | Finds methods with no callers (Java-aware exemptions) | Cleanup audits |
568
569
  | Change coupling | Analyses git history for co-changed file pairs | `get_change_coupling`, `related` |
569
570
 
570
- **Fast default:** `codespine analyse` prioritizes a queryable core index. Communities, flows, dead-code, git coupling, and cross-module links are queued in a detached background enrichment job unless you use `--complete`.
571
+ **Fast default:** `codespine analyse` prioritizes a queryable core index and keeps the foreground run inside the configured budget. If core indexing or call/type resolution cannot finish in time, the partial index is published and a background continuation finishes the core graph. Communities, flows, dead-code, git coupling, and cross-module links are queued in a detached background enrichment job unless you use `--complete`.
571
572
 
572
573
  **Health checks:** every analyse run now performs a small self-test query suite and reports index anomalies such as large projects with zero call edges. Use `codespine health` for the terminal dashboard or `codespine self-test --json` in CI.
573
574
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codespine"
7
- version = "1.0.10"
7
+ version = "1.0.11"
8
8
  description = "Local Java code intelligence indexer backed by a graph database"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -32,3 +32,15 @@ def test_background_command_shows_progress():
32
32
  assert task_id in result.output
33
33
  assert "40%" in result.output
34
34
  assert "execution flows" in result.output
35
+
36
+
37
+ def test_background_command_shows_recent_finished_tasks_by_default():
38
+ task_id = create_task("indexing", "Background core indexing", path="/tmp/project")
39
+ finish_task(task_id, "failed", "boom")
40
+
41
+ result = CliRunner().invoke(main, ["background"])
42
+
43
+ assert result.exit_code == 0
44
+ assert task_id in result.output
45
+ assert "failed" in result.output
46
+ assert "boom" in result.output
File without changes
File without changes
File without changes