agent-coderag 1.3.5__tar.gz → 1.4.0__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 (113) hide show
  1. {agent_coderag-1.3.5/agent_coderag.egg-info → agent_coderag-1.4.0}/PKG-INFO +24 -4
  2. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/README.md +23 -3
  3. {agent_coderag-1.3.5 → agent_coderag-1.4.0/agent_coderag.egg-info}/PKG-INFO +24 -4
  4. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/agent_coderag.egg-info/SOURCES.txt +14 -4
  5. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/__init__.py +6 -1
  6. agent_coderag-1.4.0/code_rag/api/client.py +236 -0
  7. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/core/constants.py +1 -0
  8. agent_coderag-1.4.0/code_rag/core/error_codes.py +7 -0
  9. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/core/exceptions.py +15 -0
  10. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/discovery/manager.py +4 -0
  11. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/discovery/providers/java.py +6 -2
  12. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/entry/cli.py +54 -48
  13. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/intelligence/distiller.py +20 -5
  14. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/intelligence/factory.py +2 -1
  15. agent_coderag-1.4.0/code_rag/paths.py +20 -0
  16. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/services/config.py +36 -4
  17. agent_coderag-1.4.0/code_rag/services/dependencies.py +167 -0
  18. agent_coderag-1.4.0/code_rag/services/discovery_api.py +11 -0
  19. agent_coderag-1.4.0/code_rag/services/factory.py +16 -0
  20. agent_coderag-1.4.0/code_rag/services/indexing.py +212 -0
  21. agent_coderag-1.4.0/code_rag/services/search.py +9 -0
  22. agent_coderag-1.4.0/code_rag/services/sync.py +159 -0
  23. agent_coderag-1.4.0/code_rag/storage/db_connection.py +115 -0
  24. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/storage/duckdb_impl.py +149 -63
  25. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/pyproject.toml +1 -1
  26. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_api_client.py +49 -32
  27. agent_coderag-1.4.0/tests/test_api_lazy_db.py +100 -0
  28. agent_coderag-1.4.0/tests/test_api_rebuild_wipe.py +187 -0
  29. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_cli.py +48 -102
  30. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_cli_detailed.py +90 -70
  31. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_cli_embeddings.py +8 -18
  32. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_cli_json_parity.py +14 -13
  33. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_code_rag_simple.py +16 -10
  34. agent_coderag-1.4.0/tests/test_coderag_lifetime.py +320 -0
  35. agent_coderag-1.4.0/tests/test_db_connection.py +302 -0
  36. agent_coderag-1.4.0/tests/test_db_path.py +36 -0
  37. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_distiller.py +45 -27
  38. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_distiller_config_embedding.py +2 -2
  39. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_distiller_extra.py +10 -6
  40. agent_coderag-1.4.0/tests/test_error_codes.py +21 -0
  41. agent_coderag-1.4.0/tests/test_factory_async.py +53 -0
  42. agent_coderag-1.3.5/tests/test_manager_detailed.py → agent_coderag-1.4.0/tests/test_indexing_detailed.py +46 -46
  43. agent_coderag-1.3.5/tests/test_manager_embeddings.py → agent_coderag-1.4.0/tests/test_indexing_embeddings.py +51 -23
  44. agent_coderag-1.3.5/tests/test_manager_extra.py → agent_coderag-1.4.0/tests/test_indexing_extra.py +23 -19
  45. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_readme_library_usage.py +1 -0
  46. agent_coderag-1.4.0/tests/test_readme_lifetime_storage.py +35 -0
  47. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_storage_detailed.py +7 -2
  48. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_storage_embeddings.py +214 -43
  49. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_sync_worker_errors.py +111 -79
  50. agent_coderag-1.3.5/code_rag/api/client.py +0 -110
  51. agent_coderag-1.3.5/code_rag/core/manager.py +0 -351
  52. agent_coderag-1.3.5/code_rag/services/discovery_api.py +0 -8
  53. agent_coderag-1.3.5/code_rag/services/factory.py +0 -30
  54. agent_coderag-1.3.5/code_rag/services/search.py +0 -6
  55. agent_coderag-1.3.5/code_rag/services/sync.py +0 -115
  56. agent_coderag-1.3.5/tests/test_api_rebuild_wipe.py +0 -171
  57. agent_coderag-1.3.5/tests/test_factory_async.py +0 -95
  58. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/LICENSE +0 -0
  59. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/NOTICE +0 -0
  60. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/agent_coderag.egg-info/dependency_links.txt +0 -0
  61. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/agent_coderag.egg-info/entry_points.txt +0 -0
  62. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/agent_coderag.egg-info/requires.txt +0 -0
  63. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/agent_coderag.egg-info/top_level.txt +0 -0
  64. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/api/__init__.py +0 -0
  65. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/api/models.py +0 -0
  66. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/core/__init__.py +0 -0
  67. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/core/interfaces.py +0 -0
  68. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/core/models.py +0 -0
  69. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/core/utils.py +0 -0
  70. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/discovery/__init__.py +0 -0
  71. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/discovery/dependency.py +0 -0
  72. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/discovery/java_discovery.py +0 -0
  73. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/discovery/providers/__init__.py +0 -0
  74. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/discovery/providers/base.py +0 -0
  75. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/discovery/providers/csharp.py +0 -0
  76. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/discovery/providers/go.py +0 -0
  77. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/discovery/providers/javascript.py +0 -0
  78. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/discovery/providers/python.py +0 -0
  79. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/discovery/providers/rust.py +0 -0
  80. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/entry/__init__.py +0 -0
  81. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/intelligence/__init__.py +0 -0
  82. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/intelligence/embedder.py +0 -0
  83. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/intelligence/openai_embedder.py +0 -0
  84. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/parsers/__init__.py +0 -0
  85. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/parsers/languages.py +0 -0
  86. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/parsers/multi_parser.py +0 -0
  87. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/parsers/tree_sitter.py +0 -0
  88. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/services/__init__.py +0 -0
  89. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/services/setup.py +0 -0
  90. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/code_rag/storage/__init__.py +0 -0
  91. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/setup.cfg +0 -0
  92. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_api_models.py +0 -0
  93. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_csharp_discovery_detailed.py +0 -0
  94. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_dependency_discovery.py +0 -0
  95. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_discovery_manager_detailed.py +0 -0
  96. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_discovery_providers_extra.py +0 -0
  97. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_discovery_python.py +0 -0
  98. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_embedder.py +0 -0
  99. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_embedder_detailed.py +0 -0
  100. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_embedder_factory.py +0 -0
  101. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_embedder_interface.py +0 -0
  102. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_embedder_stubs.py +0 -0
  103. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_interfaces.py +0 -0
  104. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_js_discovery_detailed.py +0 -0
  105. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_languages.py +0 -0
  106. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_local_onnx_embedder.py +0 -0
  107. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_models.py +0 -0
  108. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_multi_parser.py +0 -0
  109. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_openai_embedder.py +0 -0
  110. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_readme_offline_embeddings.py +0 -0
  111. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_rust_discovery_detailed.py +0 -0
  112. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_tree_sitter_parser.py +0 -0
  113. {agent_coderag-1.3.5 → agent_coderag-1.4.0}/tests/test_utils_detailed.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-coderag
3
- Version: 1.3.5
3
+ Version: 1.4.0
4
4
  Summary: Lightweight semantic code search and distillation utility for AI coding agents. It solves the API knowledge gap via real-time local signature extraction and intent analysis without PyTorch. Optimized for token efficiency, it compresses codebase context into compact semantic summaries stored in a local DuckDB vector similarity index.
5
5
  Author-email: Igor Boloban <naranor@gmail.com>
6
6
  License: MIT
@@ -110,7 +110,7 @@ agent-coderag setup
110
110
  agent-coderag config --url "http://localhost:11434" --provider "ollama" --model "qwen2.5-coder"
111
111
 
112
112
  # Using OpenAI-compatible API (e.g. Groq, OpenRouter, DeepSeek)
113
- agent-coderag config --url "https://api.deepseek.com" --key "your-api-key" --model "deepseek-chat"
113
+ agent-coderag config --url "https://api.deepseek.com" --provider "openai" --key "your-api-key" --model "deepseek-chat"
114
114
  ```
115
115
 
116
116
  ### Offline Mode (No Provider)
@@ -163,17 +163,37 @@ agent-coderag api fmt
163
163
 
164
164
  ## Library Usage
165
165
 
166
+ > **1.4.0 migration:** Storage lifetime and default DB resolution changed. Pin `agent-coderag<1.4` until you adapt (see [Database & lifetime](#database--lifetime)).
167
+
166
168
  ```python
167
- from code_rag import CodeRAG
169
+ from pathlib import Path
170
+
171
+ from code_rag import CodeRAG, default_db_path
168
172
 
169
173
  async def main():
170
- async with CodeRAG(db="code_rag.db") as rag:
174
+ root = Path(".")
175
+ print(default_db_path(root)) # resolved path before first sync
176
+
177
+ # db=None (default): legacy code_rag.db in cwd/root, else root/.coderag.db
178
+ async with CodeRAG(root=root) as rag:
171
179
  await rag.setup()
172
180
  await rag.sync(index_all=True)
173
181
  hits = await rag.search("authentication middleware", limit=5)
182
+ # Opens the DB only when a provider needs it (e.g. Java JAR cache).
174
183
  report = await rag.api("pydantic", lang="python")
175
184
  ```
176
185
 
186
+ ### Database & lifetime
187
+
188
+ - **Default path (`db=None`):** resolution order is (1) `./code_rag.db` if it exists, (2) else `{root}/code_rag.db` if it exists, (3) else `{root}/.coderag.db` (created on first `sync`/`rebuild`). Use `default_db_path(root)` to preview. New projects: prefer `.coderag.db` (step 3) or set `db=` explicitly.
189
+ - **Explicit path:** `CodeRAG(db=...)` / `agent-coderag --db ...`. Path is a **file**, not a directory. Relative paths resolve against **process cwd**, not `root`.
190
+ - **Sidecars:** DuckDB may write WAL sidecars (e.g. `.coderag.db.wal`) beside the index during writes; locks should not persist after an operation finishes.
191
+ - **Connect timeout:** `connect_timeout_seconds=5` (CLI `--connect-timeout`) waits on file locks, then raises `StorageBusyError` (`ErrorCode.STORAGE_BUSY`). Pass `0` for a single attempt.
192
+ - **Read-only search:** `search` (and `api` when storage is needed) opens read-only. A missing index file is an error — use `sync`/`rebuild` to create it. With `--json`, success is a hit array; errors are `{"status":"error","message":...}` (same shape as `sync`/`api` failures).
193
+ - **Lifetime:** embedder/parser/distiller stay warm; DuckDB opens per operation and closes afterward. One `CodeRAG` instance serializes overlapping ops. `config()` with embedding flags / `--clear-embedding` closes the process embedder so the next op rebuilds it; distill-only `config` refreshes Distiller and keeps the embedder.
194
+ - **`api()` without DB:** providers that do not need the index (e.g. Python) skip DuckDB entirely; Java uses a short read-only open for JAR cache lookup.
195
+ - **Errors:** catch `CodeRAGError` and inspect `.code` — `STORAGE_BUSY`, `STORAGE_CORRUPT`, `EMBEDDING_MISMATCH` (`from code_rag import ErrorCode`).
196
+
177
197
  ---
178
198
 
179
199
  ## Supported Ecosystems (Discovery)
@@ -64,7 +64,7 @@ agent-coderag setup
64
64
  agent-coderag config --url "http://localhost:11434" --provider "ollama" --model "qwen2.5-coder"
65
65
 
66
66
  # Using OpenAI-compatible API (e.g. Groq, OpenRouter, DeepSeek)
67
- agent-coderag config --url "https://api.deepseek.com" --key "your-api-key" --model "deepseek-chat"
67
+ agent-coderag config --url "https://api.deepseek.com" --provider "openai" --key "your-api-key" --model "deepseek-chat"
68
68
  ```
69
69
 
70
70
  ### Offline Mode (No Provider)
@@ -117,17 +117,37 @@ agent-coderag api fmt
117
117
 
118
118
  ## Library Usage
119
119
 
120
+ > **1.4.0 migration:** Storage lifetime and default DB resolution changed. Pin `agent-coderag<1.4` until you adapt (see [Database & lifetime](#database--lifetime)).
121
+
120
122
  ```python
121
- from code_rag import CodeRAG
123
+ from pathlib import Path
124
+
125
+ from code_rag import CodeRAG, default_db_path
122
126
 
123
127
  async def main():
124
- async with CodeRAG(db="code_rag.db") as rag:
128
+ root = Path(".")
129
+ print(default_db_path(root)) # resolved path before first sync
130
+
131
+ # db=None (default): legacy code_rag.db in cwd/root, else root/.coderag.db
132
+ async with CodeRAG(root=root) as rag:
125
133
  await rag.setup()
126
134
  await rag.sync(index_all=True)
127
135
  hits = await rag.search("authentication middleware", limit=5)
136
+ # Opens the DB only when a provider needs it (e.g. Java JAR cache).
128
137
  report = await rag.api("pydantic", lang="python")
129
138
  ```
130
139
 
140
+ ### Database & lifetime
141
+
142
+ - **Default path (`db=None`):** resolution order is (1) `./code_rag.db` if it exists, (2) else `{root}/code_rag.db` if it exists, (3) else `{root}/.coderag.db` (created on first `sync`/`rebuild`). Use `default_db_path(root)` to preview. New projects: prefer `.coderag.db` (step 3) or set `db=` explicitly.
143
+ - **Explicit path:** `CodeRAG(db=...)` / `agent-coderag --db ...`. Path is a **file**, not a directory. Relative paths resolve against **process cwd**, not `root`.
144
+ - **Sidecars:** DuckDB may write WAL sidecars (e.g. `.coderag.db.wal`) beside the index during writes; locks should not persist after an operation finishes.
145
+ - **Connect timeout:** `connect_timeout_seconds=5` (CLI `--connect-timeout`) waits on file locks, then raises `StorageBusyError` (`ErrorCode.STORAGE_BUSY`). Pass `0` for a single attempt.
146
+ - **Read-only search:** `search` (and `api` when storage is needed) opens read-only. A missing index file is an error — use `sync`/`rebuild` to create it. With `--json`, success is a hit array; errors are `{"status":"error","message":...}` (same shape as `sync`/`api` failures).
147
+ - **Lifetime:** embedder/parser/distiller stay warm; DuckDB opens per operation and closes afterward. One `CodeRAG` instance serializes overlapping ops. `config()` with embedding flags / `--clear-embedding` closes the process embedder so the next op rebuilds it; distill-only `config` refreshes Distiller and keeps the embedder.
148
+ - **`api()` without DB:** providers that do not need the index (e.g. Python) skip DuckDB entirely; Java uses a short read-only open for JAR cache lookup.
149
+ - **Errors:** catch `CodeRAGError` and inspect `.code` — `STORAGE_BUSY`, `STORAGE_CORRUPT`, `EMBEDDING_MISMATCH` (`from code_rag import ErrorCode`).
150
+
131
151
  ---
132
152
 
133
153
  ## Supported Ecosystems (Discovery)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-coderag
3
- Version: 1.3.5
3
+ Version: 1.4.0
4
4
  Summary: Lightweight semantic code search and distillation utility for AI coding agents. It solves the API knowledge gap via real-time local signature extraction and intent analysis without PyTorch. Optimized for token efficiency, it compresses codebase context into compact semantic summaries stored in a local DuckDB vector similarity index.
5
5
  Author-email: Igor Boloban <naranor@gmail.com>
6
6
  License: MIT
@@ -110,7 +110,7 @@ agent-coderag setup
110
110
  agent-coderag config --url "http://localhost:11434" --provider "ollama" --model "qwen2.5-coder"
111
111
 
112
112
  # Using OpenAI-compatible API (e.g. Groq, OpenRouter, DeepSeek)
113
- agent-coderag config --url "https://api.deepseek.com" --key "your-api-key" --model "deepseek-chat"
113
+ agent-coderag config --url "https://api.deepseek.com" --provider "openai" --key "your-api-key" --model "deepseek-chat"
114
114
  ```
115
115
 
116
116
  ### Offline Mode (No Provider)
@@ -163,17 +163,37 @@ agent-coderag api fmt
163
163
 
164
164
  ## Library Usage
165
165
 
166
+ > **1.4.0 migration:** Storage lifetime and default DB resolution changed. Pin `agent-coderag<1.4` until you adapt (see [Database & lifetime](#database--lifetime)).
167
+
166
168
  ```python
167
- from code_rag import CodeRAG
169
+ from pathlib import Path
170
+
171
+ from code_rag import CodeRAG, default_db_path
168
172
 
169
173
  async def main():
170
- async with CodeRAG(db="code_rag.db") as rag:
174
+ root = Path(".")
175
+ print(default_db_path(root)) # resolved path before first sync
176
+
177
+ # db=None (default): legacy code_rag.db in cwd/root, else root/.coderag.db
178
+ async with CodeRAG(root=root) as rag:
171
179
  await rag.setup()
172
180
  await rag.sync(index_all=True)
173
181
  hits = await rag.search("authentication middleware", limit=5)
182
+ # Opens the DB only when a provider needs it (e.g. Java JAR cache).
174
183
  report = await rag.api("pydantic", lang="python")
175
184
  ```
176
185
 
186
+ ### Database & lifetime
187
+
188
+ - **Default path (`db=None`):** resolution order is (1) `./code_rag.db` if it exists, (2) else `{root}/code_rag.db` if it exists, (3) else `{root}/.coderag.db` (created on first `sync`/`rebuild`). Use `default_db_path(root)` to preview. New projects: prefer `.coderag.db` (step 3) or set `db=` explicitly.
189
+ - **Explicit path:** `CodeRAG(db=...)` / `agent-coderag --db ...`. Path is a **file**, not a directory. Relative paths resolve against **process cwd**, not `root`.
190
+ - **Sidecars:** DuckDB may write WAL sidecars (e.g. `.coderag.db.wal`) beside the index during writes; locks should not persist after an operation finishes.
191
+ - **Connect timeout:** `connect_timeout_seconds=5` (CLI `--connect-timeout`) waits on file locks, then raises `StorageBusyError` (`ErrorCode.STORAGE_BUSY`). Pass `0` for a single attempt.
192
+ - **Read-only search:** `search` (and `api` when storage is needed) opens read-only. A missing index file is an error — use `sync`/`rebuild` to create it. With `--json`, success is a hit array; errors are `{"status":"error","message":...}` (same shape as `sync`/`api` failures).
193
+ - **Lifetime:** embedder/parser/distiller stay warm; DuckDB opens per operation and closes afterward. One `CodeRAG` instance serializes overlapping ops. `config()` with embedding flags / `--clear-embedding` closes the process embedder so the next op rebuilds it; distill-only `config` refreshes Distiller and keeps the embedder.
194
+ - **`api()` without DB:** providers that do not need the index (e.g. Python) skip DuckDB entirely; Java uses a short read-only open for JAR cache lookup.
195
+ - **Errors:** catch `CodeRAGError` and inspect `.code` — `STORAGE_BUSY`, `STORAGE_CORRUPT`, `EMBEDDING_MISMATCH` (`from code_rag import ErrorCode`).
196
+
177
197
  ---
178
198
 
179
199
  ## Supported Ecosystems (Discovery)
@@ -9,14 +9,15 @@ agent_coderag.egg-info/entry_points.txt
9
9
  agent_coderag.egg-info/requires.txt
10
10
  agent_coderag.egg-info/top_level.txt
11
11
  code_rag/__init__.py
12
+ code_rag/paths.py
12
13
  code_rag/api/__init__.py
13
14
  code_rag/api/client.py
14
15
  code_rag/api/models.py
15
16
  code_rag/core/__init__.py
16
17
  code_rag/core/constants.py
18
+ code_rag/core/error_codes.py
17
19
  code_rag/core/exceptions.py
18
20
  code_rag/core/interfaces.py
19
- code_rag/core/manager.py
20
21
  code_rag/core/models.py
21
22
  code_rag/core/utils.py
22
23
  code_rag/discovery/__init__.py
@@ -44,14 +45,18 @@ code_rag/parsers/multi_parser.py
44
45
  code_rag/parsers/tree_sitter.py
45
46
  code_rag/services/__init__.py
46
47
  code_rag/services/config.py
48
+ code_rag/services/dependencies.py
47
49
  code_rag/services/discovery_api.py
48
50
  code_rag/services/factory.py
51
+ code_rag/services/indexing.py
49
52
  code_rag/services/search.py
50
53
  code_rag/services/setup.py
51
54
  code_rag/services/sync.py
52
55
  code_rag/storage/__init__.py
56
+ code_rag/storage/db_connection.py
53
57
  code_rag/storage/duckdb_impl.py
54
58
  tests/test_api_client.py
59
+ tests/test_api_lazy_db.py
55
60
  tests/test_api_models.py
56
61
  tests/test_api_rebuild_wipe.py
57
62
  tests/test_cli.py
@@ -59,7 +64,10 @@ tests/test_cli_detailed.py
59
64
  tests/test_cli_embeddings.py
60
65
  tests/test_cli_json_parity.py
61
66
  tests/test_code_rag_simple.py
67
+ tests/test_coderag_lifetime.py
62
68
  tests/test_csharp_discovery_detailed.py
69
+ tests/test_db_connection.py
70
+ tests/test_db_path.py
63
71
  tests/test_dependency_discovery.py
64
72
  tests/test_discovery_manager_detailed.py
65
73
  tests/test_discovery_providers_extra.py
@@ -72,18 +80,20 @@ tests/test_embedder_detailed.py
72
80
  tests/test_embedder_factory.py
73
81
  tests/test_embedder_interface.py
74
82
  tests/test_embedder_stubs.py
83
+ tests/test_error_codes.py
75
84
  tests/test_factory_async.py
85
+ tests/test_indexing_detailed.py
86
+ tests/test_indexing_embeddings.py
87
+ tests/test_indexing_extra.py
76
88
  tests/test_interfaces.py
77
89
  tests/test_js_discovery_detailed.py
78
90
  tests/test_languages.py
79
91
  tests/test_local_onnx_embedder.py
80
- tests/test_manager_detailed.py
81
- tests/test_manager_embeddings.py
82
- tests/test_manager_extra.py
83
92
  tests/test_models.py
84
93
  tests/test_multi_parser.py
85
94
  tests/test_openai_embedder.py
86
95
  tests/test_readme_library_usage.py
96
+ tests/test_readme_lifetime_storage.py
87
97
  tests/test_readme_offline_embeddings.py
88
98
  tests/test_rust_discovery_detailed.py
89
99
  tests/test_storage_detailed.py
@@ -2,7 +2,9 @@ from code_rag.api.client import CodeRAG
2
2
  from code_rag.api.models import SyncResult, ApiReport, SetupResult
3
3
  from code_rag.core.models import KnowledgeUnit, UnitKind
4
4
  from code_rag.intelligence.distiller import DistillerConfig
5
- from code_rag.core.exceptions import CodeRAGError
5
+ from code_rag.core.error_codes import ErrorCode
6
+ from code_rag.core.exceptions import CodeRAGError, StorageBusyError
7
+ from code_rag.paths import default_db_path
6
8
 
7
9
  __all__ = [
8
10
  "CodeRAG",
@@ -13,4 +15,7 @@ __all__ = [
13
15
  "UnitKind",
14
16
  "DistillerConfig",
15
17
  "CodeRAGError",
18
+ "ErrorCode",
19
+ "StorageBusyError",
20
+ "default_db_path",
16
21
  ]
@@ -0,0 +1,236 @@
1
+ import asyncio
2
+ import logging
3
+ from contextlib import asynccontextmanager
4
+ from pathlib import Path
5
+ from typing import AsyncIterator, Optional, Union
6
+
7
+ from code_rag.api.models import ApiReport, SetupResult, SyncResult
8
+ from code_rag.core.constants import DEFAULT_CONNECT_TIMEOUT_SECONDS
9
+ from code_rag.core.interfaces import IEmbedder, IIntelligence, IParser
10
+ from code_rag.core.models import KnowledgeUnit
11
+ from code_rag.discovery.manager import DiscoveryManager
12
+ from code_rag.discovery.providers.java import JavaDiscoveryProvider
13
+ from code_rag.intelligence.distiller import Distiller, DistillerConfig
14
+ from code_rag.paths import resolve_db_path
15
+ from code_rag.services.config import (
16
+ distill_fields_requested,
17
+ embedding_fields_requested,
18
+ load_or_update_config,
19
+ )
20
+ from code_rag.services.discovery_api import run_api
21
+ from code_rag.services.factory import create_stack
22
+ from code_rag.services.search import run_search
23
+ from code_rag.services.setup import run_setup
24
+ from code_rag.services.indexing import IndexStack
25
+ from code_rag.services.sync import SyncOptions, run_rebuild, run_sync
26
+ from code_rag.storage.db_connection import AccessMode, open_db_connection
27
+ from code_rag.storage.duckdb_impl import DuckDBStorage
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class CodeRAG: # pylint: disable=too-many-instance-attributes
33
+ """Public async facade: process-scoped stack + ephemeral DB per operation."""
34
+
35
+ def __init__( # pylint: disable=too-many-arguments
36
+ self,
37
+ db: Optional[str] = None,
38
+ onnx: Optional[str] = None,
39
+ *,
40
+ root: Optional[Union[str, Path]] = None,
41
+ allow_build_execution: bool = False,
42
+ connect_timeout_seconds: float = DEFAULT_CONNECT_TIMEOUT_SECONDS,
43
+ ):
44
+ self._onnx = onnx
45
+ self._root = Path(root) if root is not None else Path.cwd()
46
+ self._allow_build_execution = allow_build_execution
47
+ self._connect_timeout_seconds = connect_timeout_seconds
48
+ self._db_path = resolve_db_path(db, root=self._root)
49
+ self._embedder: Optional[IEmbedder] = None
50
+ self._parser: Optional[IParser] = None
51
+ self._distiller: Optional[IIntelligence] = None
52
+ self._storage: Optional[DuckDBStorage] = None
53
+ self._op_lock = asyncio.Lock()
54
+ logger.info("Resolved database path: %s", self._db_path.resolve())
55
+
56
+ async def __aenter__(self) -> "CodeRAG":
57
+ return self
58
+
59
+ async def __aexit__(self, *_exc) -> None:
60
+ await self.close()
61
+
62
+ async def _ensure_stack(self) -> None:
63
+ if self._embedder is not None:
64
+ return
65
+ self._embedder, self._parser, self._distiller = await create_stack(self._onnx)
66
+
67
+ @asynccontextmanager
68
+ async def _with_storage(
69
+ self, mode: AccessMode, *, wipe: bool = False
70
+ ) -> AsyncIterator[DuckDBStorage]:
71
+ async with self._op_lock:
72
+ await self._ensure_stack()
73
+ if self._embedder is None:
74
+ raise RuntimeError("process stack is not initialized")
75
+ storage = await open_db_connection(
76
+ self._db_path,
77
+ self._embedder,
78
+ mode=mode,
79
+ connect_timeout_seconds=self._connect_timeout_seconds,
80
+ wipe=wipe,
81
+ )
82
+ self._storage = storage
83
+ try:
84
+ yield storage
85
+ finally:
86
+ await storage.close()
87
+ self._storage = None
88
+
89
+ @asynccontextmanager
90
+ async def _metadata_ro_connection(self) -> AsyncIterator[DuckDBStorage]:
91
+ storage = await open_db_connection(
92
+ self._db_path,
93
+ None,
94
+ mode=AccessMode.READ_ONLY,
95
+ connect_timeout_seconds=self._connect_timeout_seconds,
96
+ )
97
+ self._storage = storage
98
+ try:
99
+ yield storage
100
+ finally:
101
+ await storage.close()
102
+ self._storage = None
103
+
104
+ async def close(self) -> None:
105
+ async with self._op_lock:
106
+ if self._storage is not None:
107
+ await self._storage.close()
108
+ self._storage = None
109
+ if self._embedder is not None:
110
+ await self._embedder.close()
111
+ self._embedder = None
112
+ self._parser = None
113
+ self._distiller = None
114
+
115
+ async def config( # pylint: disable=too-many-arguments
116
+ self,
117
+ *,
118
+ url: Optional[str] = None,
119
+ key: Optional[str] = None,
120
+ model: Optional[str] = None,
121
+ provider: Optional[str] = None,
122
+ embedding_url: Optional[str] = None,
123
+ embedding_key: Optional[str] = None,
124
+ embedding_model: Optional[str] = None,
125
+ embedding_provider: Optional[str] = None,
126
+ clear_embedding: bool = False,
127
+ ) -> DistillerConfig:
128
+ touch_embedding = embedding_fields_requested(
129
+ embedding_url=embedding_url,
130
+ embedding_key=embedding_key,
131
+ embedding_model=embedding_model,
132
+ embedding_provider=embedding_provider,
133
+ clear_embedding=clear_embedding,
134
+ )
135
+ touch_distill = distill_fields_requested(
136
+ url=url, key=key, model=model, provider=provider
137
+ )
138
+ result = load_or_update_config(
139
+ url=url,
140
+ key=key,
141
+ model=model,
142
+ provider=provider,
143
+ embedding_url=embedding_url,
144
+ embedding_key=embedding_key,
145
+ embedding_model=embedding_model,
146
+ embedding_provider=embedding_provider,
147
+ clear_embedding=clear_embedding,
148
+ )
149
+ if touch_embedding or touch_distill:
150
+ await self._invalidate_stack_after_config(
151
+ embedding=touch_embedding, distill=touch_distill
152
+ )
153
+ return result
154
+
155
+ async def _invalidate_stack_after_config(
156
+ self, *, embedding: bool, distill: bool
157
+ ) -> None:
158
+ """Refresh process stack pieces affected by config().
159
+
160
+ Embedding changes close and drop the embedder (full stack rebuild on
161
+ next op). Distill-only changes replace Distiller and keep the embedder.
162
+ """
163
+ async with self._op_lock:
164
+ if embedding and self._embedder is not None:
165
+ await self._embedder.close()
166
+ self._embedder = None
167
+ self._parser = None
168
+ self._distiller = None
169
+ return
170
+ if distill and self._distiller is not None:
171
+ self._distiller = Distiller(DistillerConfig.load())
172
+
173
+ async def setup(self, *, force: bool = False) -> SetupResult:
174
+ return await run_setup(force=force)
175
+
176
+ async def sync(
177
+ self,
178
+ path: Optional[str] = None,
179
+ *,
180
+ index_all: bool = False,
181
+ force: bool = False,
182
+ ) -> SyncResult:
183
+ if path is None and not index_all:
184
+ return SyncResult(status="success", indexed_files=0)
185
+ async with self._with_storage(AccessMode.READ_WRITE) as storage:
186
+ if self._parser is None or self._distiller is None:
187
+ raise RuntimeError("process stack is not initialized")
188
+ stack = IndexStack(storage, self._parser, self._distiller)
189
+ return await run_sync(
190
+ stack,
191
+ SyncOptions(
192
+ root=self._root,
193
+ path=path,
194
+ index_all=index_all,
195
+ force=force,
196
+ allow_build_execution=self._allow_build_execution,
197
+ ),
198
+ )
199
+
200
+ async def search(self, query: str, *, limit: int = 5) -> list[KnowledgeUnit]:
201
+ async with self._with_storage(AccessMode.READ_ONLY) as storage:
202
+ return await run_search(storage, query, limit=limit)
203
+
204
+ async def _api_java(
205
+ self,
206
+ discovery: DiscoveryManager,
207
+ provider: JavaDiscoveryProvider,
208
+ library: str,
209
+ language: str,
210
+ ) -> ApiReport:
211
+ async with self._op_lock:
212
+ async with self._metadata_ro_connection() as storage:
213
+ provider.storage = storage
214
+ try:
215
+ return await run_api(discovery, library, lang=language)
216
+ finally:
217
+ provider.storage = None
218
+
219
+ async def api(self, library: str, *, lang: Optional[str] = None) -> ApiReport:
220
+ language = lang or "python"
221
+ discovery = DiscoveryManager()
222
+ provider = discovery.get_provider(language)
223
+ if isinstance(provider, JavaDiscoveryProvider):
224
+ return await self._api_java(discovery, provider, library, language)
225
+ return await run_api(discovery, library, lang=language)
226
+
227
+ async def rebuild(self) -> SyncResult:
228
+ async with self._with_storage(AccessMode.READ_WRITE, wipe=True) as storage:
229
+ if self._parser is None or self._distiller is None:
230
+ raise RuntimeError("process stack is not initialized")
231
+ stack = IndexStack(storage, self._parser, self._distiller)
232
+ return await run_rebuild(
233
+ stack,
234
+ root=self._root,
235
+ allow_build_execution=self._allow_build_execution,
236
+ )
@@ -14,6 +14,7 @@ PAD_TOKEN = "[PAD]" # nosec B105
14
14
 
15
15
  # Performance & Timeout Constants
16
16
  DEFAULT_SUBPROCESS_TIMEOUT = 30
17
+ DEFAULT_CONNECT_TIMEOUT_SECONDS = 5.0
17
18
  METADATA_FETCH_TIMEOUT = 60
18
19
  LLM_REQUEST_TIMEOUT = 30
19
20
  MAX_CONCURRENT_TASKS = 10
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+
4
+ class ErrorCode(str, Enum):
5
+ STORAGE_BUSY = "STORAGE_BUSY"
6
+ STORAGE_CORRUPT = "STORAGE_CORRUPT"
7
+ EMBEDDING_MISMATCH = "EMBEDDING_MISMATCH"
@@ -2,15 +2,30 @@
2
2
  Custom exception hierarchy for CodeRAG.
3
3
  """
4
4
 
5
+ from typing import Optional
6
+
7
+ from .error_codes import ErrorCode
8
+
5
9
 
6
10
  class CodeRAGError(Exception):
7
11
  """Base class for all CodeRAG exceptions."""
8
12
 
13
+ def __init__(self, message: str = "", *, code: Optional[ErrorCode] = None):
14
+ super().__init__(message)
15
+ self.code = code
16
+
9
17
 
10
18
  class StorageError(CodeRAGError):
11
19
  """Raised when a storage operation fails."""
12
20
 
13
21
 
22
+ class StorageBusyError(StorageError):
23
+ """Raised when storage is locked or busy."""
24
+
25
+ def __init__(self, message: str = "Storage is busy"):
26
+ super().__init__(message, code=ErrorCode.STORAGE_BUSY)
27
+
28
+
14
29
  class ParserError(CodeRAGError):
15
30
  """Raised when code parsing fails."""
16
31
 
@@ -36,6 +36,10 @@ class DiscoveryManager:
36
36
  self._providers[language.lower()] = provider
37
37
  logger.debug("Registered discovery provider for %s", language)
38
38
 
39
+ def get_provider(self, language: str) -> Optional[IDiscoveryProvider]:
40
+ """Returns the provider for a language, if registered."""
41
+ return self._providers.get(language.lower())
42
+
39
43
  async def extract_api(self, library_name: str, language: str) -> str:
40
44
  """
41
45
  Extracts API for a library using the specified language provider.
@@ -10,6 +10,10 @@ from ...parsers.tree_sitter import TreeSitterParser
10
10
  logger = logging.getLogger(__name__)
11
11
 
12
12
 
13
+ def _cached_jar_miss_message(library_name: str) -> str:
14
+ return f"Error: Could not find cached JAR for '{library_name}'. Run 'sync' first."
15
+
16
+
13
17
  class JavaDiscoveryProvider(IDiscoveryProvider):
14
18
  """API discovery for Java libraries using bytecode/source analysis."""
15
19
 
@@ -26,11 +30,11 @@ class JavaDiscoveryProvider(IDiscoveryProvider):
26
30
  Extracts Java API from a cached JAR file.
27
31
  """
28
32
  if not self.storage:
29
- return "Error: Storage required for Java API discovery."
33
+ return _cached_jar_miss_message(library_name)
30
34
 
31
35
  jar_path = await self.storage.get_dependency_path(library_name)
32
36
  if not jar_path or not os.path.exists(jar_path):
33
- return f"Error: Could not find cached JAR for '{library_name}'. Run 'sync' first."
37
+ return _cached_jar_miss_message(library_name)
34
38
 
35
39
  output = [f"# Public API for Java Library '{library_name}':"]
36
40
  try: