source-kb 0.2.2__py3-none-any.whl

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 (228) hide show
  1. cli/__init__.py +50 -0
  2. cli/__main__.py +5 -0
  3. cli/commands/__init__.py +1 -0
  4. cli/commands/anchor_fix.py +47 -0
  5. cli/commands/diff_doc.py +52 -0
  6. cli/commands/dispatch.py +77 -0
  7. cli/commands/extract.py +72 -0
  8. cli/commands/file_list.py +74 -0
  9. cli/commands/index.py +84 -0
  10. cli/commands/lock.py +89 -0
  11. cli/commands/merge.py +60 -0
  12. cli/commands/merge_delta.py +19 -0
  13. cli/commands/metadata.py +24 -0
  14. cli/commands/pipeline.py +45 -0
  15. cli/commands/post_merge.py +43 -0
  16. cli/commands/query.py +52 -0
  17. cli/commands/render.py +101 -0
  18. cli/commands/scan_repos.py +46 -0
  19. cli/commands/setup.py +94 -0
  20. cli/commands/split.py +196 -0
  21. cli/commands/stale_files.py +98 -0
  22. cli/commands/validate.py +191 -0
  23. core/__init__.py +32 -0
  24. core/config.py +261 -0
  25. core/docs/__init__.py +7 -0
  26. core/docs/section_updater.py +286 -0
  27. core/docs/shared.py +149 -0
  28. core/git.py +294 -0
  29. core/interfaces.py +249 -0
  30. core/monitor/__init__.py +5 -0
  31. core/monitor/progress.py +83 -0
  32. core/monitor/prompt_store.py +49 -0
  33. core/paths.py +141 -0
  34. core/preset.py +237 -0
  35. core/preset_accessors.py +202 -0
  36. core/preset_classify.py +132 -0
  37. core/preset_hooks.py +129 -0
  38. core/preset_profile.py +89 -0
  39. core/prompt/__init__.py +7 -0
  40. core/prompt/__main__.py +147 -0
  41. core/prompt/content.py +320 -0
  42. core/prompt/context_manager.py +164 -0
  43. core/prompt/renderer.py +236 -0
  44. core/prompt/response_parser.py +274 -0
  45. core/prompt/templates.py +357 -0
  46. core/prompt/validate_parity.py +162 -0
  47. core/prompt/variables.py +339 -0
  48. core/rag/__init__.py +22 -0
  49. core/rag/__main__.py +136 -0
  50. core/rag/bm25_index.py +268 -0
  51. core/rag/chunker.py +273 -0
  52. core/rag/embedder.py +151 -0
  53. core/rag/indexer.py +292 -0
  54. core/rag/loader.py +89 -0
  55. core/rag/retriever.py +82 -0
  56. core/skeleton/__init__.py +11 -0
  57. core/skeleton/__main__.py +934 -0
  58. core/skeleton/anchor_fix.py +250 -0
  59. core/skeleton/classify.py +331 -0
  60. core/skeleton/cmd_anchor_fix.py +43 -0
  61. core/skeleton/cmd_diff_doc.py +44 -0
  62. core/skeleton/cmd_lock.py +87 -0
  63. core/skeleton/cmd_merge_delta.py +41 -0
  64. core/skeleton/community.py +233 -0
  65. core/skeleton/dependency_graph.py +306 -0
  66. core/skeleton/diff_doc.py +248 -0
  67. core/skeleton/dispatch.py +273 -0
  68. core/skeleton/dispatch_render.py +319 -0
  69. core/skeleton/dispatch_source.py +111 -0
  70. core/skeleton/extract.py +218 -0
  71. core/skeleton/extract_methods.py +298 -0
  72. core/skeleton/file_list.py +239 -0
  73. core/skeleton/impact.py +278 -0
  74. core/skeleton/jar_download.py +177 -0
  75. core/skeleton/jar_resolver.py +186 -0
  76. core/skeleton/loader.py +162 -0
  77. core/skeleton/merge.py +278 -0
  78. core/skeleton/merge_delta.py +229 -0
  79. core/skeleton/metadata.py +96 -0
  80. core/skeleton/metadata_builders.py +264 -0
  81. core/skeleton/module_dag.py +330 -0
  82. core/skeleton/parsers/__init__.py +71 -0
  83. core/skeleton/parsers/jqassistant.py +300 -0
  84. core/skeleton/parsers/jqassistant_cypher.py +225 -0
  85. core/skeleton/parsers/regex.py +171 -0
  86. core/skeleton/parsers/treesitter.py +324 -0
  87. core/skeleton/parsers/treesitter_java.py +284 -0
  88. core/skeleton/parsers/treesitter_multi.py +289 -0
  89. core/skeleton/pom_parser.py +299 -0
  90. core/skeleton/post_merge.py +295 -0
  91. core/skeleton/post_merge_llm.py +82 -0
  92. core/skeleton/query.py +195 -0
  93. core/skeleton/shard_context.py +177 -0
  94. core/skeleton/split.py +180 -0
  95. core/skeleton/split_cache.py +107 -0
  96. core/skeleton/split_feedback.py +174 -0
  97. core/skeleton/split_plan.py +219 -0
  98. core/skeleton/split_plan_helpers.py +305 -0
  99. core/skeleton/split_plan_llm.py +274 -0
  100. core/utils.py +135 -0
  101. core/validators/__init__.py +65 -0
  102. core/validators/__main__.py +215 -0
  103. core/validators/consistency.py +203 -0
  104. core/validators/coverage.py +171 -0
  105. core/validators/duplicates.py +76 -0
  106. core/validators/engine.py +224 -0
  107. core/validators/links.py +76 -0
  108. core/validators/sampling.py +169 -0
  109. core/validators/structure.py +144 -0
  110. engine/__init__.py +7 -0
  111. engine/assembler.py +231 -0
  112. engine/confirm.py +65 -0
  113. engine/dedup.py +106 -0
  114. engine/main.py +211 -0
  115. engine/pipeline/__init__.py +163 -0
  116. engine/pipeline/recovery.py +250 -0
  117. engine/pipeline/steps/__init__.py +23 -0
  118. engine/pipeline/steps/audit.py +220 -0
  119. engine/pipeline/steps/audit_apply.py +195 -0
  120. engine/pipeline/steps/audit_helpers.py +155 -0
  121. engine/pipeline/steps/classify_llm.py +236 -0
  122. engine/pipeline/steps/classify_prompt.py +223 -0
  123. engine/pipeline/steps/finalize.py +160 -0
  124. engine/pipeline/steps/generate.py +169 -0
  125. engine/pipeline/steps/generate_batch.py +197 -0
  126. engine/pipeline/steps/generate_recovery.py +170 -0
  127. engine/pipeline/steps/llm_plan_split.py +253 -0
  128. engine/pipeline/steps/lock.py +64 -0
  129. engine/pipeline/steps/preflight.py +237 -0
  130. engine/pipeline/steps/preflight_adjust.py +147 -0
  131. engine/pipeline/steps/pregenerate.py +130 -0
  132. engine/pipeline/steps/quality.py +81 -0
  133. engine/pipeline/steps/skeleton.py +149 -0
  134. engine/pipeline/steps/source.py +163 -0
  135. engine/pipeline/steps/sync.py +117 -0
  136. engine/pipeline/steps/sync_finalize.py +237 -0
  137. engine/pipeline/steps/sync_update.py +341 -0
  138. engine/pipelines.py +91 -0
  139. engine/runner.py +335 -0
  140. engine/strategies/__init__.py +86 -0
  141. engine/strategies/api.py +128 -0
  142. engine/strategies/delegated.py +50 -0
  143. engine/strategies/dryrun.py +25 -0
  144. engine/two_phase.py +143 -0
  145. mcp_server/__init__.py +73 -0
  146. mcp_server/__main__.py +5 -0
  147. mcp_server/tools/__init__.py +1 -0
  148. mcp_server/tools/config.py +63 -0
  149. mcp_server/tools/discovery.py +276 -0
  150. mcp_server/tools/generation.py +184 -0
  151. mcp_server/tools/planning.py +144 -0
  152. mcp_server/tools/source.py +175 -0
  153. mcp_server/tools/validation.py +140 -0
  154. mcp_server/tools/workflow.py +166 -0
  155. mcp_server/workflow_loader.py +204 -0
  156. presets/generic/audit_dimensions.md +132 -0
  157. presets/generic/doc_types.yaml +152 -0
  158. presets/generic/preset.yaml +115 -0
  159. presets/java-spring/audit_dimensions.md +228 -0
  160. presets/java-spring/audit_dimensions.yaml +203 -0
  161. presets/java-spring/doc_types.yaml +269 -0
  162. presets/java-spring/hooks.py +122 -0
  163. presets/java-spring/preset.yaml +341 -0
  164. presets/java-spring/templates/README.md +34 -0
  165. presets/java-spring/templates/audit-system.md +15 -0
  166. presets/java-spring/templates/subagent-aop.md +105 -0
  167. presets/java-spring/templates/subagent-api.md +63 -0
  168. presets/java-spring/templates/subagent-architecture.md +111 -0
  169. presets/java-spring/templates/subagent-async-events.md +107 -0
  170. presets/java-spring/templates/subagent-audit-api-contracts.md +40 -0
  171. presets/java-spring/templates/subagent-audit-architecture.md +38 -0
  172. presets/java-spring/templates/subagent-audit-business.md +40 -0
  173. presets/java-spring/templates/subagent-audit-data-models.md +40 -0
  174. presets/java-spring/templates/subagent-business.md +129 -0
  175. presets/java-spring/templates/subagent-caching.md +75 -0
  176. presets/java-spring/templates/subagent-database-access.md +114 -0
  177. presets/java-spring/templates/subagent-enum.md +75 -0
  178. presets/java-spring/templates/subagent-error-handling.md +91 -0
  179. presets/java-spring/templates/subagent-external-integrations.md +80 -0
  180. presets/java-spring/templates/subagent-index.md +122 -0
  181. presets/java-spring/templates/subagent-messaging.md +97 -0
  182. presets/java-spring/templates/subagent-model.md +88 -0
  183. presets/java-spring/templates/subagent-observability.md +91 -0
  184. presets/java-spring/templates/subagent-scheduled.md +81 -0
  185. presets/java-spring/templates/subagent-security.md +102 -0
  186. presets/java-spring/templates/subagent-structure.md +101 -0
  187. presets/java-spring/templates/subagent-sync-section.md +34 -0
  188. presets/java-spring/templates/subagent-utils.md +73 -0
  189. presets/java-spring/templates/sync-system.md +8 -0
  190. presets/java-spring/workflow-extensions.md +112 -0
  191. skills/__init__.py +1 -0
  192. skills/_shared/README.md +30 -0
  193. skills/_shared/doc-coverage-shared.md +134 -0
  194. skills/_shared/doc-quality-standard.md +1058 -0
  195. skills/_shared/doc-subagent-rules.md +762 -0
  196. skills/_shared/windows-compat.md +89 -0
  197. skills/kb-audit/SKILL.md +52 -0
  198. skills/kb-audit/rules.md +88 -0
  199. skills/kb-audit/steps/step-01-prepare.md +75 -0
  200. skills/kb-audit/steps/step-02-audit.md +96 -0
  201. skills/kb-audit/steps/step-03-verify.md +65 -0
  202. skills/kb-audit/steps/step-04-report.md +64 -0
  203. skills/kb-init/SKILL.md +146 -0
  204. skills/kb-init/rules.md +187 -0
  205. skills/kb-init/steps/step-01-scope.md +62 -0
  206. skills/kb-init/steps/step-02-source.md +410 -0
  207. skills/kb-init/steps/step-03-generate.md +307 -0
  208. skills/kb-init/steps/step-04-quality.md +92 -0
  209. skills/kb-init/steps/step-05-finalize.md +132 -0
  210. skills/kb-init/templates/core/execution-modes.md +29 -0
  211. skills/kb-init/templates/core/output-only.md +4 -0
  212. skills/kb-init/templates/core/readwrite.md +33 -0
  213. skills/kb-search/SKILL.md +138 -0
  214. skills/kb-search/rules.md +64 -0
  215. skills/kb-sync/SKILL.md +43 -0
  216. skills/kb-sync/rules.md +70 -0
  217. skills/kb-sync/scripts/rebuild_module.py +91 -0
  218. skills/kb-sync/scripts/scan_repos.py +687 -0
  219. skills/kb-sync/steps/step-01-detect.md +72 -0
  220. skills/kb-sync/steps/step-02-update.md +71 -0
  221. skills/kb-sync/steps/step-03-verify.md +47 -0
  222. skills/kb-sync/steps/step-04-finalize.md +52 -0
  223. source_kb-0.2.2.dist-info/METADATA +194 -0
  224. source_kb-0.2.2.dist-info/RECORD +228 -0
  225. source_kb-0.2.2.dist-info/WHEEL +5 -0
  226. source_kb-0.2.2.dist-info/entry_points.txt +3 -0
  227. source_kb-0.2.2.dist-info/licenses/LICENSE +21 -0
  228. source_kb-0.2.2.dist-info/top_level.txt +6 -0
core/rag/indexer.py ADDED
@@ -0,0 +1,292 @@
1
+ from __future__ import annotations
2
+
3
+ """Index builder — writes chunks to ChromaDB vector database.
4
+
5
+ Migrated from engine/rag/indexer.py. Removes print() calls and global config;
6
+ accepts Config object explicitly. Supports checkpoint resume, chunk change
7
+ detection, batch embedding, and incremental rebuild.
8
+ """
9
+
10
+ import gc
11
+ import hashlib
12
+ import json
13
+ import logging
14
+ import random
15
+ import time
16
+ import traceback
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING
19
+
20
+ import chromadb
21
+ from filelock import FileLock
22
+
23
+ from core.rag.chunker import Chunk
24
+ from core.rag.embedder import get_embeddings
25
+
26
+ if TYPE_CHECKING:
27
+ from core.config import Config
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ BATCH_SIZE = 100
32
+ _PROGRESS_VERSION = 3
33
+ _DELETE_PAGE_SIZE = 5000
34
+
35
+
36
+ def _get_client(config: "Config", kb_name: str | None = None):
37
+ """Get ChromaDB persistent client."""
38
+ chroma_path = config.chroma_dir(kb_name)
39
+ chroma_path.mkdir(parents=True, exist_ok=True)
40
+ return chromadb.PersistentClient(path=str(chroma_path))
41
+
42
+
43
+ def _progress_file(config: "Config", kb_name: str) -> Path:
44
+ """Progress file path for checkpoint resume."""
45
+ return config.chroma_dir(kb_name) / "index_progress.json"
46
+
47
+
48
+ def embed_with_retry(
49
+ texts: list[str], config: "Config", max_retries: int = 3
50
+ ) -> list[list[float]]:
51
+ """Batch embedding with retry and jitter for transient failures."""
52
+ for attempt in range(max_retries):
53
+ try:
54
+ return get_embeddings(texts, config)
55
+ except RuntimeError:
56
+ raise
57
+ except Exception as e:
58
+ if attempt < max_retries - 1:
59
+ wait = 2 ** attempt + random.uniform(0, 1)
60
+ logger.warning("Embedding retry %d/%d: %s", attempt + 1, max_retries, e)
61
+ time.sleep(wait)
62
+ else:
63
+ raise
64
+
65
+
66
+ def _embed_config_key(config: "Config") -> str:
67
+ """Return a string identifying the current embedding configuration."""
68
+ return f"{config.embed_backend}:{config.embed_model}"
69
+
70
+
71
+ def _chunk_id(source: str, index: int) -> str:
72
+ """Unified chunk ID: {source}_{index}."""
73
+ return f"{source}_{index}"
74
+
75
+
76
+ def _compute_chunks_hash(chunks: list[Chunk], config: "Config") -> str:
77
+ """Compute content fingerprint for chunks."""
78
+ h = hashlib.sha256()
79
+ h.update(_embed_config_key(config).encode())
80
+ h.update(str(len(chunks)).encode())
81
+ for c in chunks:
82
+ h.update(c.text.encode("utf-8", errors="replace"))
83
+ return h.hexdigest()
84
+
85
+
86
+ def _save_progress(config: "Config", kb_name: str, count: int, chunks_hash: str):
87
+ """Save indexing progress checkpoint."""
88
+ pf = _progress_file(config, kb_name)
89
+ lock = FileLock(str(pf) + ".lock")
90
+ with lock:
91
+ pf.write_text(json.dumps({
92
+ "version": _PROGRESS_VERSION,
93
+ "kb": kb_name, "count": count,
94
+ "chunks_hash": chunks_hash, "ts": time.time(),
95
+ }))
96
+
97
+
98
+ def _load_progress(config: "Config", kb_name: str, chunks_hash: str) -> int:
99
+ """Load progress; returns checkpoint count if hash and version match."""
100
+ try:
101
+ pf = _progress_file(config, kb_name)
102
+ lock = FileLock(str(pf) + ".lock")
103
+ with lock:
104
+ data = json.loads(pf.read_text())
105
+ if (data.get("version") == _PROGRESS_VERSION
106
+ and data.get("kb") == kb_name
107
+ and data.get("chunks_hash") == chunks_hash):
108
+ return data.get("count", 0)
109
+ except Exception as e:
110
+ logger.debug("Failed to load progress for %s: %s", kb_name, e)
111
+ return 0
112
+
113
+
114
+ def _delete_chunks_by_source(collection, source: str) -> int:
115
+ """Delete all chunks matching a specific source value (paginated)."""
116
+ deleted = 0
117
+ while True:
118
+ try:
119
+ results = collection.get(where={"source": source}, limit=_DELETE_PAGE_SIZE)
120
+ except Exception as e:
121
+ logger.debug("delete_chunks_by_source query failed for %s: %s", source, e)
122
+ break
123
+ if not results["ids"]:
124
+ break
125
+ collection.delete(ids=results["ids"])
126
+ deleted += len(results["ids"])
127
+ return deleted
128
+
129
+
130
+ def _delete_chunks_by_category(collection, category: str) -> int:
131
+ """Delete all chunks matching a category (paginated), with prefix fallback."""
132
+ deleted = 0
133
+ try:
134
+ while True:
135
+ results = collection.get(where={"category": category}, limit=_DELETE_PAGE_SIZE)
136
+ if not results["ids"]:
137
+ break
138
+ collection.delete(ids=results["ids"])
139
+ deleted += len(results["ids"])
140
+ except Exception as e:
141
+ logger.debug("delete_chunks_by_category where-query failed for %s, falling back to prefix scan: %s", category, e)
142
+ while True:
143
+ try:
144
+ all_results = collection.get(limit=_DELETE_PAGE_SIZE, include=["metadatas"])
145
+ except Exception as e2:
146
+ logger.debug("prefix scan query failed: %s", e2)
147
+ break
148
+ if not all_results["ids"]:
149
+ break
150
+ ids_to_delete = [
151
+ all_results["ids"][i]
152
+ for i, meta in enumerate(all_results["metadatas"])
153
+ if meta.get("source", "").startswith(f"{category}/")
154
+ or meta.get("source", "").startswith(f"{category}\\")
155
+ ]
156
+ if not ids_to_delete:
157
+ break
158
+ collection.delete(ids=ids_to_delete)
159
+ deleted += len(ids_to_delete)
160
+ if len(all_results["ids"]) < _DELETE_PAGE_SIZE:
161
+ break
162
+ return deleted
163
+
164
+
165
+ def _insert_chunks(
166
+ collection, chunks: list[Chunk], config: "Config",
167
+ kb_name: str, start: int, chunks_hash: str,
168
+ ) -> bool:
169
+ """Embed and insert chunks into collection with checkpoint resume."""
170
+ total = len(chunks)
171
+ for i in range(start, total, BATCH_SIZE):
172
+ batch = chunks[i:i + BATCH_SIZE]
173
+ batch_end = min(i + BATCH_SIZE, total)
174
+
175
+ try:
176
+ texts = [c.text for c in batch]
177
+ embeddings = embed_with_retry(texts, config)
178
+ collection.add(
179
+ ids=[_chunk_id(c.metadata.get("source", "unknown"), j)
180
+ for j, c in enumerate(batch, start=i)],
181
+ embeddings=embeddings,
182
+ documents=texts,
183
+ metadatas=[c.metadata for c in batch],
184
+ )
185
+ except Exception as e:
186
+ logger.error("[%s] batch %d-%d failed: %s", kb_name, i, batch_end, e)
187
+ logger.debug("Traceback:\n%s", traceback.format_exc())
188
+ _save_progress(config, kb_name, i, chunks_hash)
189
+ return False
190
+
191
+ _save_progress(config, kb_name, batch_end, chunks_hash)
192
+ gc.collect()
193
+ logger.info("[%s] %d/%d", kb_name, batch_end, total)
194
+
195
+ return True
196
+
197
+
198
+ def build_index(
199
+ chunks: list[Chunk],
200
+ collection_name: str,
201
+ config: "Config",
202
+ kb_name: str = "unknown",
203
+ module: str | None = None,
204
+ files: list[str] | None = None,
205
+ ):
206
+ """Build ChromaDB index with unified chunk IDs.
207
+
208
+ Modes:
209
+ - module=None, files=None: full rebuild (delete entire collection)
210
+ - module="x": module-level rebuild (delete by category, then insert)
211
+ - module="x", files=["a.md"]: file-level rebuild (delete by source)
212
+ """
213
+ total = len(chunks)
214
+ if total == 0:
215
+ logger.info("[%s] no chunks to index", kb_name)
216
+ return
217
+
218
+ client = _get_client(config, kb_name)
219
+
220
+ if module:
221
+ _build_incremental(client, collection_name, chunks, config, kb_name, module, files)
222
+ else:
223
+ _build_full(client, collection_name, chunks, config, kb_name)
224
+
225
+
226
+ def _build_incremental(client, collection_name, chunks, config, kb_name, module, files):
227
+ """Incremental mode: get or create collection, delete targeted chunks."""
228
+ total = len(chunks)
229
+ try:
230
+ collection = client.get_collection(collection_name)
231
+ except Exception:
232
+ collection = client.get_or_create_collection(
233
+ name=collection_name, metadata={"hnsw:space": "cosine"})
234
+
235
+ if files:
236
+ deleted = 0
237
+ for fname in files:
238
+ source_posix = f"{module}/{fname}"
239
+ deleted += _delete_chunks_by_source(collection, source_posix)
240
+ logger.info("[%s] deleted %d old chunks for %d file(s)", kb_name, deleted, len(files))
241
+ else:
242
+ deleted = _delete_chunks_by_category(collection, module)
243
+ logger.info("[%s] deleted %d old chunks for module %s", kb_name, deleted, module)
244
+
245
+ chunks_hash = _compute_chunks_hash(chunks, config)
246
+ _insert_chunks(collection, chunks, config, kb_name, 0, chunks_hash)
247
+ logger.info("[%s] done! inserted %d chunks for %s", kb_name, total, module)
248
+
249
+
250
+ def _build_full(client, collection_name, chunks, config, kb_name):
251
+ """Full rebuild mode with checkpoint resume."""
252
+ total = len(chunks)
253
+ chunks_hash = _compute_chunks_hash(chunks, config)
254
+ saved = _load_progress(config, kb_name, chunks_hash)
255
+
256
+ if saved > 0:
257
+ logger.info("[%s] resuming from %d/%d", kb_name, saved, total)
258
+ collection = client.get_collection(collection_name)
259
+ start = saved
260
+ else:
261
+ logger.info("[%s] full rebuild...", kb_name)
262
+ try:
263
+ client.delete_collection(collection_name)
264
+ except Exception as e:
265
+ logger.debug("delete_collection(%s) skipped: %s", collection_name, e)
266
+ collection = client.create_collection(
267
+ name=collection_name, metadata={"hnsw:space": "cosine"})
268
+ start = 0
269
+
270
+ if start >= total:
271
+ logger.info("[%s] already complete: %d/%d", kb_name, start, total)
272
+ return
273
+
274
+ ok = _insert_chunks(collection, chunks, config, kb_name, start, chunks_hash)
275
+ if ok:
276
+ logger.info("[%s] done! %d/%d", kb_name, collection.count(), total)
277
+
278
+
279
+ def get_collection(collection_name: str, config: "Config", kb_name: str | None = None):
280
+ """Get an existing collection.
281
+
282
+ Raises:
283
+ RuntimeError: If the collection does not exist.
284
+ """
285
+ client = _get_client(config, kb_name)
286
+ try:
287
+ return client.get_collection(collection_name)
288
+ except Exception:
289
+ raise RuntimeError(
290
+ f"Collection '{collection_name}' not found. "
291
+ f"Run index build first."
292
+ )
core/rag/loader.py ADDED
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ """Document loader — loads markdown and source files from knowledge base directory.
4
+
5
+ Migrated from engine/rag/loader.py. Removes print() calls (no CLI I/O in core/).
6
+ """
7
+
8
+ import logging
9
+ from pathlib import Path
10
+ from dataclasses import dataclass
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class Document:
17
+ """A loaded document with content and metadata."""
18
+ content: str
19
+ metadata: dict
20
+
21
+
22
+ def load_documents(
23
+ knowledge_dir: str | Path,
24
+ module: str | None = None,
25
+ files: list[str] | None = None,
26
+ ) -> list[Document]:
27
+ """Recursively load .md files from a directory.
28
+
29
+ Args:
30
+ knowledge_dir: Root knowledge directory.
31
+ module: If set, only load from knowledge_dir/module/.
32
+ files: If set (requires module), only load these filenames.
33
+
34
+ Returns:
35
+ List of Document objects with content and metadata.
36
+ """
37
+ knowledge_dir = Path(knowledge_dir)
38
+ if not knowledge_dir.exists():
39
+ logger.warning("Directory does not exist: %s", knowledge_dir)
40
+ return []
41
+
42
+ if module:
43
+ scan_dir = knowledge_dir / module
44
+ if not scan_dir.exists():
45
+ logger.warning("Module directory does not exist: %s", scan_dir)
46
+ return []
47
+ else:
48
+ scan_dir = knowledge_dir
49
+
50
+ docs: list[Document] = []
51
+ for path in sorted(scan_dir.rglob("*.md")):
52
+ if path.is_symlink():
53
+ continue
54
+ if ".meta" in path.parts:
55
+ continue
56
+ if files and path.name not in files:
57
+ continue
58
+
59
+ content = _read_file(path)
60
+ if not content:
61
+ continue
62
+
63
+ rel = path.relative_to(knowledge_dir)
64
+ category = rel.parts[0] if len(rel.parts) > 1 else "general"
65
+
66
+ docs.append(Document(
67
+ content=content,
68
+ metadata={
69
+ "source": rel.as_posix(),
70
+ "category": category,
71
+ },
72
+ ))
73
+
74
+ logger.info("Loaded %d documents from %s", len(docs), scan_dir)
75
+ return docs
76
+
77
+
78
+ def _read_file(path: Path) -> str | None:
79
+ """Read a file, handling encoding automatically."""
80
+ for enc in ("utf-8", "gbk"):
81
+ try:
82
+ text = path.read_text(encoding=enc).strip()
83
+ if enc != "utf-8":
84
+ logger.warning("Used %s fallback encoding for: %s", enc, path.name)
85
+ return text if text else None
86
+ except (UnicodeDecodeError, UnicodeError):
87
+ continue
88
+ logger.error("Skipping undecodable file: %s", path.name)
89
+ return None
core/rag/retriever.py ADDED
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ """Retriever — semantic search from ChromaDB vector index.
4
+
5
+ Migrated from engine/rag/retriever.py. Removes global config dependency;
6
+ accepts Config object explicitly. Returns ranked results above similarity threshold.
7
+ """
8
+
9
+ import logging
10
+ from typing import TYPE_CHECKING
11
+
12
+ from core.rag.embedder import get_embeddings
13
+ from core.rag.indexer import get_collection
14
+
15
+ if TYPE_CHECKING:
16
+ from core.config import Config
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def retrieve(
22
+ query: str,
23
+ config: "Config",
24
+ top_k: int | None = None,
25
+ collection_name: str | None = None,
26
+ kb_name: str | None = None,
27
+ min_score: float | None = None,
28
+ ) -> list[dict]:
29
+ """Retrieve chunks most relevant to the query.
30
+
31
+ Args:
32
+ query: Query text for semantic search.
33
+ config: Config object providing retrieval settings.
34
+ top_k: Maximum number of results to return.
35
+ collection_name: ChromaDB collection name.
36
+ kb_name: Knowledge base name (used to locate ChromaDB).
37
+ min_score: Minimum similarity threshold (cosine similarity).
38
+
39
+ Returns:
40
+ List of dicts with keys: text, metadata, score.
41
+ Sorted by score descending.
42
+
43
+ Raises:
44
+ ValueError: If collection_name cannot be determined.
45
+ RuntimeError: If the collection does not exist.
46
+ """
47
+ if top_k is None:
48
+ top_k = config.top_k
49
+ if min_score is None:
50
+ min_score = config.similarity_threshold
51
+
52
+ if not collection_name:
53
+ kb_cfg = config.get_kb(kb_name) if kb_name else {}
54
+ collection_name = kb_cfg.get("collection") if isinstance(kb_cfg, dict) else None
55
+ if not collection_name:
56
+ raise ValueError(
57
+ "collection_name is required — either pass it directly or "
58
+ "configure 'collection' in the knowledge_base config"
59
+ )
60
+
61
+ collection = get_collection(collection_name, config, kb_name=kb_name)
62
+ query_embedding = get_embeddings([query], config)[0]
63
+
64
+ results = collection.query(
65
+ query_embeddings=[query_embedding],
66
+ n_results=top_k,
67
+ include=["documents", "metadatas", "distances"],
68
+ )
69
+
70
+ chunks: list[dict] = []
71
+ for i in range(len(results["ids"][0])):
72
+ score = 1 - results["distances"][0][i] # cosine similarity
73
+ if score < min_score:
74
+ continue
75
+ chunks.append({
76
+ "text": results["documents"][0][i],
77
+ "metadata": results["metadatas"][0][i],
78
+ "score": score,
79
+ })
80
+
81
+ logger.debug("Retrieved %d chunks (query=%r, top_k=%d)", len(chunks), query[:50], top_k)
82
+ return chunks
@@ -0,0 +1,11 @@
1
+ """core.skeleton — skeleton extraction, file classification, split planning, and merge.
2
+
3
+ Sub-modules:
4
+ parsers/ — Language-specific skeleton parsers (registry pattern)
5
+ extract — Skeleton extraction orchestrator
6
+ file_list — File list extraction + coverage check
7
+ classify — Rule-based + LLM-assisted file classification
8
+ split — SplitConfig + compute_splits + plan_splits
9
+ metadata — Global metadata pre-generation
10
+ merge — Shard merge (fast) + post-merge refinement
11
+ """