synap-git 2.1.2__tar.gz → 2.1.4__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 (45) hide show
  1. {synap_git-2.1.2 → synap_git-2.1.4}/CHANGELOG.md +11 -0
  2. {synap_git-2.1.2 → synap_git-2.1.4}/PKG-INFO +1 -1
  3. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/__init__.py +1 -1
  4. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/cli/main.py +46 -26
  5. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/indexer/daemon.py +13 -6
  6. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/indexer/engine.py +65 -43
  7. {synap_git-2.1.2 → synap_git-2.1.4}/.gitignore +0 -0
  8. {synap_git-2.1.2 → synap_git-2.1.4}/.synap.example/README.md +0 -0
  9. {synap_git-2.1.2 → synap_git-2.1.4}/LICENSE.md +0 -0
  10. {synap_git-2.1.2 → synap_git-2.1.4}/README.md +0 -0
  11. {synap_git-2.1.2 → synap_git-2.1.4}/pyproject.toml +0 -0
  12. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/api/__init__.py +0 -0
  13. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/api/app.py +0 -0
  14. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/api/static/index.html +0 -0
  15. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/cli/__init__.py +0 -0
  16. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/cli/__main__.py +0 -0
  17. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/config.py +0 -0
  18. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/diagnostics/__init__.py +0 -0
  19. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/diagnostics/logger.py +0 -0
  20. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/diagnostics/tracing.py +0 -0
  21. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/embeddings/__init__.py +0 -0
  22. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/git/__init__.py +0 -0
  23. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/git/state.py +0 -0
  24. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/indexer/__init__.py +0 -0
  25. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/indexer/scanner.py +0 -0
  26. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/indexer/wiki.py +0 -0
  27. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/mcp/__init__.py +0 -0
  28. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/mcp/server.py +0 -0
  29. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/parser/__init__.py +0 -0
  30. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/parser/registry.py +0 -0
  31. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/anthropic.py +0 -0
  32. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/base.py +0 -0
  33. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/factory.py +0 -0
  34. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/gemini.py +0 -0
  35. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/ollama.py +0 -0
  36. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/openai.py +0 -0
  37. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/provider/openrouter.py +0 -0
  38. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/py.typed +0 -0
  39. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/retrieval/__init__.py +0 -0
  40. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/retrieval/engine.py +0 -0
  41. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/retrieval/memory.py +0 -0
  42. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/storage/__init__.py +0 -0
  43. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/storage/sqlite.py +0 -0
  44. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/utils/__init__.py +0 -0
  45. {synap_git-2.1.2 → synap_git-2.1.4}/src/synap_git/utils/serialization.py +0 -0
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.1.4] - 2026-06-11
9
+
10
+ ### Fixed
11
+ - **Graceful Shutdown Hang**: Fixed an edge case where `synap stop` would time out and forcefully terminate due to `concurrent.futures` threads preventing immediate python process exit when handling embeddings. The CLI now safely flushes databases and exits instantly.
12
+
13
+ ## [2.1.3] - 2026-06-11
14
+
15
+ ### Changed
16
+ - **CLI Interactivity Enhancement**: Wrapped all long-running `synap` CLI commands (`init`, `start`, `stop`, `rollback`, `repair`) with interactive rich status spinners to ensure users never stare at an empty screen.
17
+ - **Incremental Indexing Progress**: Added a rich progress status to the `_incremental_index` background loop to provide visibility into parsing tasks during cache hits.
18
+
8
19
  ## [2.1.2] - 2026-06-11
9
20
 
10
21
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: synap-git
3
- Version: 2.1.2
3
+ Version: 2.1.4
4
4
  Summary: Persistent structural context infrastructure for AI coding agents.
5
5
  Project-URL: Homepage, https://github.com/saahilpal/synap-git
6
6
  Project-URL: Repository, https://github.com/saahilpal/synap-git
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "2.1.2"
5
+ __version__ = "2.1.4"
@@ -524,7 +524,13 @@ def init(
524
524
  _auto_protect_synap(settings.repository_path)
525
525
 
526
526
  runtime = SynapRuntime(settings)
527
- commit = runtime.bootstrap(force=force)
527
+ if not quiet and not json_output:
528
+ with console.status(
529
+ "[yellow]Initializing Synap runtime and scanning repository...[/yellow]", spinner="dots"
530
+ ):
531
+ commit = runtime.bootstrap(force=force)
532
+ else:
533
+ commit = runtime.bootstrap(force=force)
528
534
 
529
535
  if json_output:
530
536
  _emit({"active_commit": commit, "state": "initialized"}, json_output=True)
@@ -670,20 +676,22 @@ def start(
670
676
  console.print(f"[bold red]✗ Failed to start daemon:[/bold red] {e}")
671
677
  raise typer.Exit(1)
672
678
 
673
- console.print("[yellow]Starting Synap daemon...[/yellow]")
674
679
  success = False
675
- for _ in range(15): # wait up to 3 seconds
676
- time.sleep(0.2)
677
- if pid_file.exists():
678
- hb = _read_daemon_heartbeat(abs_path)
679
- if hb and hb.get("status") in ("healthy", "degraded"):
680
- success = True
681
- port = hb.get("port", 9876)
682
- pid = int(hb.get("pid", 0))
683
- console.print(f"[green]✓ Synap daemon started (PID {pid})[/green]")
684
- console.print("[green]✓ Runtime healthy[/green]")
685
- console.print(f"[green]✓ UI available at http://127.0.0.1:{port}[/green]")
686
- break
680
+ with console.status(
681
+ "[yellow]Starting Synap daemon and awaiting health check...[/yellow]", spinner="dots"
682
+ ):
683
+ for _ in range(15): # wait up to 3 seconds
684
+ time.sleep(0.2)
685
+ if pid_file.exists():
686
+ hb = _read_daemon_heartbeat(abs_path)
687
+ if hb and hb.get("status") in ("healthy", "degraded"):
688
+ success = True
689
+ port = hb.get("port", 9876)
690
+ pid = int(hb.get("pid", 0))
691
+ console.print(f"[green]✓ Synap daemon started (PID {pid})[/green]")
692
+ console.print("[green]✓ Runtime healthy[/green]")
693
+ console.print(f"[green]✓ UI available at http://127.0.0.1:{port}[/green]")
694
+ break
687
695
 
688
696
  if not success:
689
697
  console.print(
@@ -710,6 +718,9 @@ def daemon_run(
710
718
  daemon = RuntimeDaemon(settings)
711
719
  asyncio.run(daemon.start())
712
720
 
721
+ # Forceful process exit to prevent atexit thread-join blocking
722
+ os._exit(0)
723
+
713
724
 
714
725
  @app.command()
715
726
  def stop(
@@ -766,11 +777,15 @@ def stop(
766
777
 
767
778
  # Wait for graceful shutdown (up to 10 seconds)
768
779
  success = False
769
- for _ in range(50):
770
- time.sleep(0.2)
771
- if not _is_process_running(pid):
772
- success = True
773
- break
780
+ with console.status(
781
+ f"[yellow]Waiting for daemon (PID {pid}) to shut down gracefully...[/yellow]",
782
+ spinner="dots",
783
+ ):
784
+ for _ in range(50):
785
+ time.sleep(0.2)
786
+ if not _is_process_running(pid):
787
+ success = True
788
+ break
774
789
 
775
790
  # If graceful failed, force kill
776
791
  if not success:
@@ -1234,7 +1249,10 @@ def rollback(
1234
1249
 
1235
1250
  structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
1236
1251
 
1237
- runtime.bootstrap(force=True)
1252
+ with console.status(
1253
+ f"[yellow]Reindexing repository to match {selected_commit}...[/yellow]", spinner="dots"
1254
+ ):
1255
+ runtime.bootstrap(force=True)
1238
1256
  console.print(f"[green]✓ Rolled back successfully to {selected_commit}[/green]")
1239
1257
 
1240
1258
 
@@ -1294,15 +1312,17 @@ def repair(
1294
1312
 
1295
1313
  runtime.initialize_storage()
1296
1314
  console.print("[2/3] Reindexing all symbols")
1297
- runtime.bootstrap(force=True)
1315
+ with console.status("[yellow]Reindexing repository...[/yellow]", spinner="dots"):
1316
+ runtime.bootstrap(force=True)
1298
1317
 
1299
1318
  console.print("[3/3] Regenerating wiki from last known state")
1300
- try:
1301
- runtime.wiki.generate_project_wiki()
1302
- except Exception as e:
1303
- import structlog
1319
+ with console.status("[yellow]Regenerating wiki documentation...[/yellow]", spinner="dots"):
1320
+ try:
1321
+ runtime.wiki.generate_project_wiki()
1322
+ except Exception as e:
1323
+ import structlog
1304
1324
 
1305
- structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
1325
+ structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
1306
1326
 
1307
1327
  status_info = runtime.status()
1308
1328
  console.print(f"\n[green]✓ Recovery complete. {status_info.files} files restored.[/green]")
@@ -100,14 +100,10 @@ class RuntimeDaemon:
100
100
  self._running = False
101
101
  self.logger.info("daemon_shutting_down")
102
102
  self._ui_server.should_exit = True
103
-
104
- from synap_git.config import RuntimeProfile
105
-
106
- if self.settings.profile == RuntimeProfile.TEST:
107
- self._ui_server.force_exit = True
103
+ self._ui_server.force_exit = True # Always force exit since daemon is terminating
108
104
 
109
105
  # Allow uvicorn and wiki worker to shut down gracefully
110
- timeout = self.settings.shutdown_timeout_seconds
106
+ timeout = 0.5
111
107
  self.logger.info("daemon_waiting_for_tasks", timeout=timeout)
112
108
 
113
109
  tasks = [server_task, wiki_worker_task]
@@ -132,6 +128,17 @@ class RuntimeDaemon:
132
128
  self.logger.info("daemon_stop_requested")
133
129
  self._stop_event.set()
134
130
 
131
+ # Force immediate cleanup and exit to avoid thread-join blocking
132
+ try:
133
+ self.runtime.shutdown()
134
+ self._delete_heartbeat()
135
+ except Exception:
136
+ pass
137
+
138
+ import os
139
+
140
+ os._exit(0)
141
+
135
142
  def health(self) -> DaemonHealth:
136
143
  status = self.runtime.status()
137
144
  return DaemonHealth(
@@ -601,55 +601,77 @@ class SynapRuntime:
601
601
  # Process additions and modifications
602
602
  registry = CodeParserRegistry()
603
603
  parsed_results = []
604
- for rel_path in filtered_added_or_modified:
605
- self.logger.info("reindexing_file", path=rel_path)
606
- p = self.settings.repository_path / rel_path
607
604
 
608
- try:
609
- content = p.read_text(encoding="utf-8", errors="replace")
610
- except Exception as e:
611
- self.logger.warning("failed_to_read_file", path=rel_path, error=str(e))
612
- continue
605
+ import contextlib
613
606
 
614
- import hashlib
615
-
616
- content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
617
- file_id_hash = hashlib.sha256((rel_path + content_hash).encode("utf-8")).hexdigest()
618
-
619
- parse_result = registry.parse(p, relative_path=rel_path, text=content)
620
- symbols_list = [
621
- {
622
- "symbol_id": sym.stable_id,
623
- "name": sym.name,
624
- "kind": sym.kind,
625
- "start_line": sym.start_line,
626
- "end_line": sym.end_line,
627
- "ast_hash": sym.ast_hash,
628
- "metadata": sym.metadata,
629
- }
630
- for sym in parse_result.symbols
631
- ]
632
-
633
- self.store.upsert_file_and_symbols(
634
- file_id=file_id_hash,
635
- path=rel_path,
636
- git_oid=git_oids.get(rel_path, ""),
637
- content_hash=content_hash,
638
- language=parse_result.language,
639
- symbols=symbols_list,
640
- )
641
- self._schedule_embedding(file_id_hash, symbols_list, content_hash)
607
+ from rich.console import Console
642
608
 
643
- self.store.set_wiki_status(rel_path, None, "stale")
644
- self.store.enqueue_wiki(rel_path)
609
+ console = Console(stderr=True)
610
+ show_progress = (
611
+ self.settings.logging_mode.name == "HUMAN" and len(filtered_added_or_modified) > 0
612
+ )
645
613
 
646
- with self.store.connect() as conn:
647
- conn.execute(
648
- "DELETE FROM edges WHERE source_symbol IN (SELECT symbol_id FROM symbols WHERE file_id = ?)",
649
- (file_id_hash,),
614
+ with (
615
+ console.status(
616
+ "[yellow]Parsing incrementally changed files...[/yellow]", spinner="dots"
617
+ )
618
+ if show_progress
619
+ else contextlib.nullcontext()
620
+ ) as status_ctx:
621
+ for idx, rel_path in enumerate(filtered_added_or_modified, 1):
622
+ if show_progress and hasattr(status_ctx, "update"):
623
+ status_ctx.update( # type: ignore[union-attr]
624
+ f"[yellow]Parsing incrementally changed files ([white]{idx}/{len(filtered_added_or_modified)}[/white])...[/yellow]"
625
+ )
626
+
627
+ self.logger.info("reindexing_file", path=rel_path)
628
+ p = self.settings.repository_path / rel_path
629
+
630
+ try:
631
+ content = p.read_text(encoding="utf-8", errors="replace")
632
+ except Exception as e:
633
+ self.logger.warning("failed_to_read_file", path=rel_path, error=str(e))
634
+ continue
635
+
636
+ import hashlib
637
+
638
+ content_hash = hashlib.sha256(content.encode("utf-8")).hexdigest()
639
+ file_id_hash = hashlib.sha256((rel_path + content_hash).encode("utf-8")).hexdigest()
640
+
641
+ parse_result = registry.parse(p, relative_path=rel_path, text=content)
642
+ symbols_list = [
643
+ {
644
+ "symbol_id": sym.stable_id,
645
+ "name": sym.name,
646
+ "kind": sym.kind,
647
+ "start_line": sym.start_line,
648
+ "end_line": sym.end_line,
649
+ "ast_hash": sym.ast_hash,
650
+ "metadata": sym.metadata,
651
+ }
652
+ for sym in parse_result.symbols
653
+ ]
654
+
655
+ self.store.upsert_file_and_symbols(
656
+ file_id=file_id_hash,
657
+ path=rel_path,
658
+ git_oid=git_oids.get(rel_path, ""),
659
+ content_hash=content_hash,
660
+ language=parse_result.language,
661
+ symbols=symbols_list,
650
662
  )
663
+ self._schedule_embedding(file_id_hash, symbols_list, content_hash)
664
+
665
+ self.store.set_wiki_status(rel_path, None, "stale")
666
+ self.store.enqueue_wiki(rel_path)
667
+
668
+ with self.store.connect() as conn:
669
+ conn.execute(
670
+ "DELETE FROM edges WHERE source_symbol IN (SELECT symbol_id FROM symbols WHERE file_id = ?)",
671
+ (file_id_hash,),
672
+ )
651
673
 
652
- parsed_results.append((file_id_hash, rel_path, list(parse_result.imports)))
674
+ parsed_results.append((file_id_hash, rel_path, list(parse_result.imports)))
653
675
 
654
676
  # Re-resolve edges for changed files
655
677
  if parsed_results:
File without changes
File without changes
File without changes
File without changes