codespine 1.0.6__tar.gz → 1.0.8__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.6 → codespine-1.0.8}/PKG-INFO +1 -1
  2. {codespine-1.0.6 → codespine-1.0.8}/codespine/__init__.py +1 -1
  3. {codespine-1.0.6 → codespine-1.0.8}/codespine/cli.py +152 -4
  4. {codespine-1.0.6 → codespine-1.0.8}/codespine/indexer/engine.py +417 -212
  5. {codespine-1.0.6 → codespine-1.0.8}/codespine.egg-info/PKG-INFO +1 -1
  6. {codespine-1.0.6 → codespine-1.0.8}/codespine.egg-info/SOURCES.txt +1 -0
  7. {codespine-1.0.6 → codespine-1.0.8}/pyproject.toml +1 -1
  8. codespine-1.0.8/tests/test_parse_resilience.py +290 -0
  9. {codespine-1.0.6 → codespine-1.0.8}/LICENSE +0 -0
  10. {codespine-1.0.6 → codespine-1.0.8}/README.md +0 -0
  11. {codespine-1.0.6 → codespine-1.0.8}/codespine/analysis/__init__.py +0 -0
  12. {codespine-1.0.6 → codespine-1.0.8}/codespine/analysis/community.py +0 -0
  13. {codespine-1.0.6 → codespine-1.0.8}/codespine/analysis/context.py +0 -0
  14. {codespine-1.0.6 → codespine-1.0.8}/codespine/analysis/coupling.py +0 -0
  15. {codespine-1.0.6 → codespine-1.0.8}/codespine/analysis/crossmodule.py +0 -0
  16. {codespine-1.0.6 → codespine-1.0.8}/codespine/analysis/deadcode.py +0 -0
  17. {codespine-1.0.6 → codespine-1.0.8}/codespine/analysis/flow.py +0 -0
  18. {codespine-1.0.6 → codespine-1.0.8}/codespine/analysis/impact.py +0 -0
  19. {codespine-1.0.6 → codespine-1.0.8}/codespine/cache/__init__.py +0 -0
  20. {codespine-1.0.6 → codespine-1.0.8}/codespine/cache/result_cache.py +0 -0
  21. {codespine-1.0.6 → codespine-1.0.8}/codespine/config.py +0 -0
  22. {codespine-1.0.6 → codespine-1.0.8}/codespine/db/__init__.py +0 -0
  23. {codespine-1.0.6 → codespine-1.0.8}/codespine/db/_cypher_compat.py +0 -0
  24. {codespine-1.0.6 → codespine-1.0.8}/codespine/db/duckdb_store.py +0 -0
  25. {codespine-1.0.6 → codespine-1.0.8}/codespine/db/schema.py +0 -0
  26. {codespine-1.0.6 → codespine-1.0.8}/codespine/db/store.py +0 -0
  27. {codespine-1.0.6 → codespine-1.0.8}/codespine/diff/__init__.py +0 -0
  28. {codespine-1.0.6 → codespine-1.0.8}/codespine/diff/branch_diff.py +0 -0
  29. {codespine-1.0.6 → codespine-1.0.8}/codespine/guide.py +0 -0
  30. {codespine-1.0.6 → codespine-1.0.8}/codespine/indexer/__init__.py +0 -0
  31. {codespine-1.0.6 → codespine-1.0.8}/codespine/indexer/call_resolver.py +0 -0
  32. {codespine-1.0.6 → codespine-1.0.8}/codespine/indexer/di_resolver.py +0 -0
  33. {codespine-1.0.6 → codespine-1.0.8}/codespine/indexer/java_parser.py +0 -0
  34. {codespine-1.0.6 → codespine-1.0.8}/codespine/indexer/symbol_builder.py +0 -0
  35. {codespine-1.0.6 → codespine-1.0.8}/codespine/mcp/__init__.py +0 -0
  36. {codespine-1.0.6 → codespine-1.0.8}/codespine/mcp/server.py +0 -0
  37. {codespine-1.0.6 → codespine-1.0.8}/codespine/noise/__init__.py +0 -0
  38. {codespine-1.0.6 → codespine-1.0.8}/codespine/noise/blocklist.py +0 -0
  39. {codespine-1.0.6 → codespine-1.0.8}/codespine/overlay/__init__.py +0 -0
  40. {codespine-1.0.6 → codespine-1.0.8}/codespine/overlay/git_state.py +0 -0
  41. {codespine-1.0.6 → codespine-1.0.8}/codespine/overlay/merge.py +0 -0
  42. {codespine-1.0.6 → codespine-1.0.8}/codespine/overlay/store.py +0 -0
  43. {codespine-1.0.6 → codespine-1.0.8}/codespine/search/__init__.py +0 -0
  44. {codespine-1.0.6 → codespine-1.0.8}/codespine/search/bm25.py +0 -0
  45. {codespine-1.0.6 → codespine-1.0.8}/codespine/search/fuzzy.py +0 -0
  46. {codespine-1.0.6 → codespine-1.0.8}/codespine/search/hybrid.py +0 -0
  47. {codespine-1.0.6 → codespine-1.0.8}/codespine/search/rrf.py +0 -0
  48. {codespine-1.0.6 → codespine-1.0.8}/codespine/search/vector.py +0 -0
  49. {codespine-1.0.6 → codespine-1.0.8}/codespine/sharding/__init__.py +0 -0
  50. {codespine-1.0.6 → codespine-1.0.8}/codespine/sharding/router.py +0 -0
  51. {codespine-1.0.6 → codespine-1.0.8}/codespine/sharding/store.py +0 -0
  52. {codespine-1.0.6 → codespine-1.0.8}/codespine/watch/__init__.py +0 -0
  53. {codespine-1.0.6 → codespine-1.0.8}/codespine/watch/git_hook.py +0 -0
  54. {codespine-1.0.6 → codespine-1.0.8}/codespine/watch/watcher.py +0 -0
  55. {codespine-1.0.6 → codespine-1.0.8}/codespine.egg-info/dependency_links.txt +0 -0
  56. {codespine-1.0.6 → codespine-1.0.8}/codespine.egg-info/entry_points.txt +0 -0
  57. {codespine-1.0.6 → codespine-1.0.8}/codespine.egg-info/requires.txt +0 -0
  58. {codespine-1.0.6 → codespine-1.0.8}/codespine.egg-info/top_level.txt +0 -0
  59. {codespine-1.0.6 → codespine-1.0.8}/gindex.py +0 -0
  60. {codespine-1.0.6 → codespine-1.0.8}/setup.cfg +0 -0
  61. {codespine-1.0.6 → codespine-1.0.8}/tests/test_branch_diff_normalize.py +0 -0
  62. {codespine-1.0.6 → codespine-1.0.8}/tests/test_call_resolver.py +0 -0
  63. {codespine-1.0.6 → codespine-1.0.8}/tests/test_community_detection.py +0 -0
  64. {codespine-1.0.6 → codespine-1.0.8}/tests/test_cypher_compat.py +0 -0
  65. {codespine-1.0.6 → codespine-1.0.8}/tests/test_deadcode.py +0 -0
  66. {codespine-1.0.6 → codespine-1.0.8}/tests/test_duckdb_store.py +0 -0
  67. {codespine-1.0.6 → codespine-1.0.8}/tests/test_index_and_hybrid.py +0 -0
  68. {codespine-1.0.6 → codespine-1.0.8}/tests/test_java_parser.py +0 -0
  69. {codespine-1.0.6 → codespine-1.0.8}/tests/test_multimodule_index.py +0 -0
  70. {codespine-1.0.6 → codespine-1.0.8}/tests/test_overlay.py +0 -0
  71. {codespine-1.0.6 → codespine-1.0.8}/tests/test_result_cache.py +0 -0
  72. {codespine-1.0.6 → codespine-1.0.8}/tests/test_search_ranking.py +0 -0
  73. {codespine-1.0.6 → codespine-1.0.8}/tests/test_sharding.py +0 -0
  74. {codespine-1.0.6 → codespine-1.0.8}/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.6
3
+ Version: 1.0.8
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.6"
4
+ __version__ = "1.0.8"
@@ -137,10 +137,18 @@ def _index_shard_group(
137
137
 
138
138
  for mod_path, project_id in modules:
139
139
  # Per-module progress state (local — no shared mutation).
140
- parse_state: dict = {"shown": False, "indexed": 0, "total": 0,
141
- "last_ts": 0.0, "printed_zero": False}
140
+ parse_state: dict = {
141
+ "shown": False, "indexed": 0, "total": 0,
142
+ "last_ts": 0.0, "printed_zero": False,
143
+ "current_file": "", "elapsed": 0.0,
144
+ "last_done": -1, "frozen_since": 0.0, "stall_warned": False,
145
+ }
142
146
  call_state: dict = {"shown": False, "count": 0, "last_ts": 0.0,
143
147
  "started_at": 0.0}
148
+ db_state: dict = {
149
+ "shown": False, "done": 0, "total": 0, "last_ts": 0.0,
150
+ "started_at": 0.0,
151
+ }
144
152
 
145
153
  def _progress(event: str, payload: dict) -> None:
146
154
  now = time.perf_counter()
@@ -160,11 +168,61 @@ def _index_shard_group(
160
168
  _phase(f"{prefix}Parsing code...", "0/0")
161
169
  parse_state["printed_zero"] = True
162
170
  return
171
+ if event == "parse_heartbeat":
172
+ # Fires every 2s from a daemon thread — keeps spinner alive
173
+ # even when all worker threads are busy or one is hanging.
174
+ done = int(payload.get("done", 0))
175
+ total = int(payload.get("total", 0))
176
+ current = str(payload.get("current_file", ""))
177
+ elapsed_s = float(payload.get("elapsed", 0.0))
178
+ parse_state["indexed"] = done
179
+ parse_state["total"] = total
180
+ parse_state["current_file"] = current
181
+ parse_state["elapsed"] = elapsed_s
182
+ if total > 0 and not parallel:
183
+ basename = os.path.basename(current) if current else ""
184
+ click.echo(
185
+ f"\r{_spinner_char()} {prefix}Parsing code... "
186
+ f"{_bar(done, total)} {done}/{total} "
187
+ f"{basename[:38]:<38} {elapsed_s:.0f}s ",
188
+ nl=False,
189
+ )
190
+ parse_state["shown"] = True
191
+ parse_state["last_ts"] = now
192
+
193
+ # ── Stall detection ──────────────────────────────────────
194
+ if done == parse_state["last_done"]:
195
+ if parse_state["frozen_since"] == 0.0:
196
+ parse_state["frozen_since"] = now
197
+ stalled_for = now - parse_state["frozen_since"]
198
+ if stalled_for >= 15.0 and not parse_state["stall_warned"]:
199
+ parse_state["stall_warned"] = True
200
+ basename = os.path.basename(current) if current else "unknown"
201
+ with output_lock:
202
+ click.echo() # break out of \r line
203
+ click.secho(
204
+ f" ⚠ Parsing stalled on {basename} for "
205
+ f"{stalled_for:.0f}s — file may be pathological.\n"
206
+ f" Timeout at {os.environ.get('CODESPINE_PARSE_TIMEOUT_SECS', '60')}s. "
207
+ f"To skip large files: "
208
+ f"export CODESPINE_MAX_FILE_BYTES=2097152",
209
+ fg="yellow",
210
+ )
211
+ else:
212
+ parse_state["last_done"] = done
213
+ parse_state["frozen_since"] = 0.0
214
+ parse_state["stall_warned"] = False
215
+ return
163
216
  if event == "parse_progress":
164
217
  indexed = int(payload.get("indexed", 0))
165
218
  total = int(payload.get("total", 0))
166
219
  parse_state["indexed"] = indexed
167
220
  parse_state["total"] = total
221
+ # Reset stall tracker on actual progress
222
+ if indexed != parse_state["last_done"]:
223
+ parse_state["last_done"] = indexed
224
+ parse_state["frozen_since"] = 0.0
225
+ parse_state["stall_warned"] = False
168
226
  if total == 0:
169
227
  return
170
228
  if indexed == total or (now - parse_state["last_ts"]) >= 0.2:
@@ -183,11 +241,101 @@ def _index_shard_group(
183
241
  parse_state["shown"] = True
184
242
  parse_state["last_ts"] = now
185
243
  return
186
- if event in ("resolve_calls_start",):
244
+ if event == "db_write_start":
187
245
  if parse_state["shown"]:
188
246
  with output_lock:
189
247
  click.echo()
190
248
  parse_state["shown"] = False
249
+ total = int(payload.get("total", 0))
250
+ deleted = int(payload.get("deleted_files", 0))
251
+ db_state["done"] = 0
252
+ db_state["total"] = total
253
+ db_state["started_at"] = now
254
+ status = f"starting ({total} files"
255
+ if deleted:
256
+ status += f", {deleted} deleted"
257
+ status += ")"
258
+ with output_lock:
259
+ _phase(f"{prefix}Writing index...", status)
260
+ return
261
+ if event == "db_write_heartbeat":
262
+ done = int(payload.get("done", 0))
263
+ total = int(payload.get("total", 0))
264
+ classes = int(payload.get("classes", 0))
265
+ methods = int(payload.get("methods", 0))
266
+ phase = str(payload.get("phase", "writing"))
267
+ elapsed_s = float(payload.get("elapsed", 0.0))
268
+ db_state["done"] = done
269
+ db_state["total"] = total
270
+ if not parallel:
271
+ click.echo(
272
+ f"\r{_spinner_char()} {prefix}Writing index... "
273
+ f"{_bar(done, total)} {done}/{total} "
274
+ f"{classes} classes / {methods} methods "
275
+ f"{phase[:18]:<18} {elapsed_s:.0f}s ",
276
+ nl=False,
277
+ )
278
+ else:
279
+ with output_lock:
280
+ click.echo(
281
+ f"\r{prefix}Writing {done}/{total} "
282
+ f"({classes} classes, {methods} methods, {elapsed_s:.0f}s) ",
283
+ nl=False,
284
+ )
285
+ db_state["shown"] = True
286
+ db_state["last_ts"] = now
287
+ return
288
+ if event == "db_write_progress":
289
+ done = int(payload.get("done", 0))
290
+ total = int(payload.get("total", 0))
291
+ classes = int(payload.get("classes", 0))
292
+ methods = int(payload.get("methods", 0))
293
+ phase = str(payload.get("phase", "writing"))
294
+ db_state["done"] = done
295
+ db_state["total"] = total
296
+ if total == 0 and done == 0:
297
+ return
298
+ if done == total or (now - db_state["last_ts"]) >= 0.25:
299
+ elapsed_s = now - db_state["started_at"]
300
+ if not parallel:
301
+ click.echo(
302
+ f"\r{_spinner_char()} {prefix}Writing index... "
303
+ f"{_bar(done, total)} {done}/{total} "
304
+ f"{classes} classes / {methods} methods "
305
+ f"{phase[:18]:<18} {elapsed_s:.0f}s ",
306
+ nl=False,
307
+ )
308
+ else:
309
+ with output_lock:
310
+ click.echo(
311
+ f"\r{prefix}Writing {done}/{total} "
312
+ f"({classes} classes, {methods} methods, {elapsed_s:.0f}s) ",
313
+ nl=False,
314
+ )
315
+ db_state["shown"] = True
316
+ db_state["last_ts"] = now
317
+ return
318
+ if event == "db_write_done":
319
+ if db_state["shown"]:
320
+ with output_lock:
321
+ click.echo()
322
+ db_state["shown"] = False
323
+ files = int(payload.get("files_indexed", db_state["done"]))
324
+ classes = int(payload.get("classes", 0))
325
+ methods = int(payload.get("methods", 0))
326
+ elapsed_s = float(payload.get("elapsed", 0.0))
327
+ with output_lock:
328
+ _phase(
329
+ f"{prefix}Writing index...",
330
+ f"{files} files, {classes} classes, {methods} methods ({elapsed_s:.1f}s)",
331
+ )
332
+ return
333
+ if event in ("resolve_calls_start",):
334
+ if parse_state["shown"] or db_state["shown"]:
335
+ with output_lock:
336
+ click.echo()
337
+ parse_state["shown"] = False
338
+ db_state["shown"] = False
191
339
  call_state["started_at"] = now
192
340
  with output_lock:
193
341
  _phase(f"{prefix}Tracing calls...", "starting...")
@@ -255,7 +403,7 @@ def _index_shard_group(
255
403
  total_files += result.files_found
256
404
 
257
405
  # Flush any dangling progress line.
258
- if parse_state["shown"]:
406
+ if parse_state["shown"] or db_state["shown"]:
259
407
  with output_lock:
260
408
  click.echo()
261
409