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
@@ -0,0 +1,687 @@
1
+ #!/usr/bin/env python3
2
+ """仓库变动扫描脚本 - 重构版
3
+
4
+ 从 kb-project.yaml 读取配置,使用 core 模块进行 git 操作和文件分类。
5
+
6
+ 用法:
7
+ python scan_repos.py --init # 初始化状态
8
+ python scan_repos.py # 扫描变动
9
+ python scan_repos.py --kb my-project # 只扫描指定知识库
10
+ python scan_repos.py --module user-service # 只扫描指定模块
11
+ python scan_repos.py --update-state --kb my-project --module user-service --commit <hash> # 更新基线
12
+ python scan_repos.py --batch-update # 批量更新基线
13
+ python scan_repos.py --force # 强制扫描
14
+ python scan_repos.py --config <path> # 指定配置文件
15
+ """
16
+
17
+ import sys
18
+ import json
19
+ import argparse
20
+ from pathlib import Path
21
+ from typing import Optional
22
+ from datetime import datetime
23
+
24
+ # 确保 stdout 使用 UTF-8 编码
25
+ sys.stdout.reconfigure(encoding="utf-8")
26
+
27
+ # 添加项目根目录到 sys.path
28
+ SCRIPT_DIR = Path(__file__).resolve().parent
29
+ PROJECT_ROOT = SCRIPT_DIR.parent.parent.parent # source-kb/
30
+ if str(PROJECT_ROOT) not in sys.path:
31
+ sys.path.append(str(PROJECT_ROOT))
32
+
33
+ try:
34
+ from core.config import load_config
35
+ from core.git import (
36
+ ensure_repo, get_head_commit,
37
+ diff_files, diff_stat, FileLock
38
+ )
39
+ from core.preset import classify_file, get_affected_docs, load_preset
40
+ except ImportError as e:
41
+ print(f"错误: 无法导入 core 模块: {e}", file=sys.stderr)
42
+ print("请确保在 source-kb/ 目录下运行此脚本", file=sys.stderr)
43
+ sys.exit(1)
44
+
45
+
46
+ # ============================================================================
47
+ # 配置和状态管理
48
+ # ============================================================================
49
+
50
+ def get_state_file(config: dict) -> Path:
51
+ """获取状态文件路径
52
+
53
+ 状态文件放在 kb-project.yaml 同目录下的 .source-kb/sync-state.json
54
+
55
+ Args:
56
+ config: 完整配置字典
57
+
58
+ Returns:
59
+ 状态文件路径
60
+ """
61
+ # 从配置中获取任意一个 knowledge_dir,推导出项目根目录
62
+ kbs = config.get("knowledge_bases", {})
63
+ if not kbs:
64
+ raise ValueError("配置中没有知识库")
65
+
66
+ # 使用第一个知识库的 knowledge_dir 的父目录
67
+ first_kb = next(iter(kbs.values()))
68
+ kb_dir = Path(first_kb["knowledge_dir"])
69
+ project_root = kb_dir.parent
70
+
71
+ state_dir = project_root / ".source-kb"
72
+ state_dir.mkdir(parents=True, exist_ok=True)
73
+
74
+ return state_dir / "sync-state.json"
75
+
76
+
77
+ def get_report_file(config: dict) -> Path:
78
+ """获取报告文件路径
79
+
80
+ 报告文件放在 .source-kb/sync-report.json
81
+
82
+ Args:
83
+ config: 完整配置字典
84
+
85
+ Returns:
86
+ 报告文件路径
87
+ """
88
+ state_file = get_state_file(config)
89
+ return state_file.parent / "sync-report.json"
90
+
91
+
92
+ def load_state(config: dict) -> dict:
93
+ """加载状态文件
94
+
95
+ Args:
96
+ config: 完整配置字典
97
+
98
+ Returns:
99
+ 状态字典,格式: {kb_name: {module_name: commit_hash}}
100
+ """
101
+ state_file = get_state_file(config)
102
+
103
+ if not state_file.exists():
104
+ return {}
105
+
106
+ try:
107
+ with open(state_file, "r", encoding="utf-8") as f:
108
+ return json.load(f)
109
+ except Exception as e:
110
+ print(f"警告: 读取状态文件失败: {e}", file=sys.stderr)
111
+ return {}
112
+
113
+
114
+ def save_state(config: dict, state: dict):
115
+ """保存状态文件
116
+
117
+ Args:
118
+ config: 完整配置字典
119
+ state: 状态字典
120
+ """
121
+ state_file = get_state_file(config)
122
+
123
+ try:
124
+ with open(state_file, "w", encoding="utf-8") as f:
125
+ json.dump(state, f, indent=2, ensure_ascii=False)
126
+ except Exception as e:
127
+ print(f"错误: 保存状态文件失败: {e}", file=sys.stderr)
128
+ sys.exit(1)
129
+
130
+
131
+ def save_report(config: dict, report: dict):
132
+ """保存扫描报告
133
+
134
+ Args:
135
+ config: 完整配置字典
136
+ report: 报告字典
137
+ """
138
+ report_file = get_report_file(config)
139
+
140
+ try:
141
+ with open(report_file, "w", encoding="utf-8") as f:
142
+ json.dump(report, f, indent=2, ensure_ascii=False)
143
+ print(f"\n报告已保存: {report_file}")
144
+ except Exception as e:
145
+ print(f"警告: 保存报告文件失败: {e}", file=sys.stderr)
146
+
147
+
148
+ # ============================================================================
149
+ # 模块和分类器加载
150
+ # ============================================================================
151
+
152
+ def get_modules(kb_config: dict) -> list[dict]:
153
+ """从 kb 配置提取模块列表
154
+
155
+ multi-repo: 从 source.repos 读取
156
+ monorepo: 从 source.modules 读取
157
+
158
+ Args:
159
+ fig: 知识库配置
160
+
161
+ Returns:
162
+ 模块列表,格式: [{"name": "xxx", "branch": "main", "url": "...", "local": "...", "type": "service"}]
163
+ """
164
+ source = kb_config.get("source", {})
165
+ structure = source.get("structure")
166
+
167
+ if structure == "multi-repo":
168
+ repos = source.get("repos", [])
169
+ return [
170
+ {
171
+ "name": repo.get("name", "unknown"),
172
+ "branch": repo.get("branch", "main"),
173
+ "url": repo.get("url"),
174
+ "local": repo.get("local"),
175
+ "type": repo.get("type", "service")
176
+ }
177
+ for repo in repos
178
+ ]
179
+
180
+ elif structure == "monorepo":
181
+ modules = source.get("modules", [])
182
+ base_url = source.get("url")
183
+ base_local = source.get("local")
184
+ base_branch = source.get("branch", "main")
185
+
186
+ return [
187
+ {
188
+ "name": module.get("name", "unknown"),
189
+ "branch": base_branch,
190
+ "url": base_url,
191
+ "local": base_local,
192
+ "type": module.get("type", "service"),
193
+ "path": module.get("path") # monorepo 特有
194
+ }
195
+ for module in modules
196
+ ]
197
+
198
+ else:
199
+ raise ValueError(f"不支持的 source.structure: {structure}")
200
+
201
+
202
+ def classify_files_to_docs(preset: dict, file_paths: list[str]) -> list[str]:
203
+ """对文件列表进行分类,返回受影响的文档文件名列表
204
+
205
+ 使用 core.preset 的 classify_file + get_affected_docs。
206
+
207
+ Args:
208
+ preset: preset 配置字典
209
+ file_paths: 文件相对路径列表
210
+
211
+ Returns:
212
+ 受影响的文档文件名列表(去重)
213
+ """
214
+ all_categories = []
215
+ for fp in file_paths:
216
+ cats = classify_file(preset, fp)
217
+ all_categories.extend(cats)
218
+
219
+ if not all_categories:
220
+ return []
221
+
222
+ return sorted(get_affected_docs(preset, all_categories))
223
+
224
+
225
+ # ============================================================================
226
+ # 扫描逻辑
227
+ # ============================================================================
228
+
229
+ def scan_module(
230
+ kb_name: str,
231
+ module_config: dict,
232
+ old_commit: Optional[str],
233
+ cache_dir: Path,
234
+ preset: dict
235
+ ) -> dict:
236
+ """扫描单个模块的变动
237
+
238
+ Args:
239
+ kb_name: 知识库名称
240
+ module_config: 模块配置
241
+ old_commit: 旧 commit(None 表示初始化)
242
+ cache_dir: 缓存目录
243
+ preset: preset 配置字典
244
+
245
+ Returns:
246
+ 变动信息字典
247
+ """
248
+ module_name = module_config["name"]
249
+ branch = module_config.get("branch", "main")
250
+
251
+ # 确保本地有可用的仓库
252
+ try:
253
+ repo_path = ensure_repo(
254
+ url=module_config.get("url"),
255
+ local=module_config.get("local"),
256
+ cache_dir=cache_dir,
257
+ module_name=module_name,
258
+ branch=branch
259
+ )
260
+ except Exception as e:
261
+ return {
262
+ "error": str(e),
263
+ "module": module_name
264
+ }
265
+
266
+ # 获取当前 commit
267
+ ref = f"origin/{branch}"
268
+ new_commit = get_head_commit(repo_path, ref)
269
+
270
+ if new_commit is None:
271
+ return {
272
+ "error": f"无法获取 {ref} 的 commit",
273
+ "module": module_name
274
+ }
275
+
276
+ # 如果没有旧 commit,返回初始化信息
277
+ if old_commit is None:
278
+ return {
279
+ "module": module_name,
280
+ "status": "initialized",
281
+ "commit": new_commit,
282
+ "branch": branch
283
+ }
284
+
285
+ # 如果 commit 没有变化
286
+ if old_commit == new_commit:
287
+ return {
288
+ "module": module_name,
289
+ "status": "unchanged",
290
+ "commit": new_commit
291
+ }
292
+
293
+ # 获取变动文件列表
294
+ try:
295
+ changes = diff_files(repo_path, old_commit, new_commit)
296
+ stat = diff_stat(repo_path, old_commit, new_commit)
297
+ except Exception as e:
298
+ return {
299
+ "error": f"获取 diff 失败: {e}",
300
+ "module": module_name,
301
+ "old_commit": old_commit,
302
+ "new_commit": new_commit
303
+ }
304
+
305
+ # 文件分类
306
+ changed_files = [c["file"] for c in changes]
307
+ affected_docs = classify_files_to_docs(preset, changed_files)
308
+
309
+ return {
310
+ "module": module_name,
311
+ "status": "changed",
312
+ "old_commit": old_commit,
313
+ "new_commit": new_commit,
314
+ "branch": branch,
315
+ "changes": changes,
316
+ "stat": stat,
317
+ "affected_docs": affected_docs,
318
+ "file_count": len(changed_files)
319
+ }
320
+
321
+
322
+ def scan(
323
+ config: dict,
324
+ filter_kb: Optional[str] = None,
325
+ filter_module: Optional[str] = None,
326
+ force: bool = False
327
+ ) -> dict:
328
+ """扫描变动
329
+
330
+ Args:
331
+ config: load_config() 返回的完整配置
332
+ filter_kb: 只扫描指定知识库
333
+ filter_module: 只扫描指定模块
334
+ force: 强制扫描(忽略状态文件)
335
+
336
+ Returns:
337
+ 扫描结果,格式: {kb_name: {module_name: {变动信息}}}
338
+ """
339
+ # 加载状态
340
+ state = {} if force else load_state(config)
341
+
342
+ results = {}
343
+
344
+ # 遍历知识库
345
+ kbs = config.get("knowledge_bases", {})
346
+ for kb_name, kb_config in kbs.items():
347
+ # 过滤知识库
348
+ if filter_kb and kb_name != filter_kb:
349
+ continue
350
+
351
+ print(f"\n扫描知识库: {kb_name}")
352
+
353
+ # 获取知识库级锁
354
+ kb_dir = Path(kb_config["knowledge_dir"])
355
+ lock = FileLock(kb_dir / ".kb-lock")
356
+
357
+ with lock:
358
+ # 获取缓存目录(从 kb 配置读取,默认 .source-cache)
359
+ source = kb_config.get("source", {})
360
+ cache_dir = Path(source.get("cache_dir", "./.source-cache"))
361
+ if not cache_dir.is_absolute():
362
+ cache_dir = PROJECT_ROOT / cache_dir
363
+ cache_dir.mkdir(parents=True, exist_ok=True)
364
+
365
+ # 加载 preset 配置
366
+ preset_name = kb_config.get("preset")
367
+ try:
368
+ preset = load_preset(preset_name)
369
+ except Exception as e:
370
+ print(f" 错误: 加载 preset 失败: {e}", file=sys.stderr)
371
+ results[kb_name] = {"error": f"加载 preset 失败: {e}"}
372
+ continue
373
+
374
+ # 获取模块列表
375
+ try:
376
+ modules = get_modules(kb_config)
377
+ except Exception as e:
378
+ print(f" 错误: 获取模块列表失败: {e}", file=sys.stderr)
379
+ results[kb_name] = {"error": f"获取模块列表失败: {e}"}
380
+ continue
381
+
382
+ kb_results = {}
383
+
384
+ # 遍历模块
385
+ for module_config in modules:
386
+ module_name = module_config["name"]
387
+
388
+ # 过滤模块
389
+ if filter_module and module_name != filter_module:
390
+ continue
391
+
392
+ print(f" 模块: {module_name}")
393
+
394
+ # 获取旧 commit
395
+ old_commit = state.get(kb_name, {}).get(module_name)
396
+
397
+ # 扫描模块
398
+ result = scan_module(
399
+ kb_name,
400
+ module_config,
401
+ old_commit,
402
+ cache_dir,
403
+ preset
404
+ )
405
+
406
+ kb_results[module_name] = result
407
+
408
+ # 打印结果
409
+ if "error" in result:
410
+ print(f" 错误: {result['error']}")
411
+ elif result["status"] == "initialized":
412
+ print(f" 初始化: {result['commit'][:8]}")
413
+ elif result["status"] == "unchanged":
414
+ print(f" 无变动: {result['commit'][:8]}")
415
+ elif result["status"] == "changed":
416
+ print(f" 变动: {result['old_commit'][:8]} -> {result['new_commit'][:8]}")
417
+ print(f" 文件: {result['file_count']} 个")
418
+ print(f" 影响文档: {', '.join(result['affected_docs']) if result['affected_docs'] else '无'}")
419
+
420
+ results[kb_name] = kb_results
421
+
422
+ return results
423
+
424
+
425
+ # ============================================================================
426
+ # CLI 命令
427
+ # ============================================================================
428
+
429
+ def cmd_init(config: dict):
430
+ """初始化状态文件"""
431
+ print("初始化状态...")
432
+
433
+ state = {}
434
+
435
+ kbs = config.get("knowledge_bases", {})
436
+ for kb_name, kb_config in kbs.items():
437
+ print(f"\n知识库: {kb_name}")
438
+
439
+ # 从 kb 配置读取缓存目录(与 scan() 一致)
440
+ source = kb_config.get("source", {})
441
+ cache_dir = Path(source.get("cache_dir", "./.source-cache"))
442
+ if not cache_dir.is_absolute():
443
+ cache_dir = PROJECT_ROOT / cache_dir
444
+ cache_dir.mkdir(parents=True, exist_ok=True)
445
+
446
+ modules = get_modules(kb_config)
447
+ kb_state = {}
448
+
449
+ for module_config in modules:
450
+ module_name = module_config["name"]
451
+ branch = module_config.get("branch", "main")
452
+
453
+ print(f" 模块: {module_name}")
454
+
455
+ try:
456
+ repo_path = ensure_repo(
457
+ url=module_config.get("url"),
458
+ local=module_config.get("local"),
459
+ cache_dir=cache_dir,
460
+ module_name=module_name,
461
+ branch=branch
462
+ )
463
+
464
+ ref = f"origin/{branch}"
465
+ commit = get_head_commit(repo_path, ref)
466
+
467
+ if commit:
468
+ kb_state[module_name] = commit
469
+ print(f" 初始化: {commit[:8]}")
470
+ else:
471
+ print(f" 错误: 无法获取 commit")
472
+
473
+ except Exception as e:
474
+ print(f" 错误: {e}")
475
+
476
+ state[kb_name] = kb_state
477
+
478
+ save_state(config, state)
479
+ print(f"\n状态已保存: {get_state_file(config)}")
480
+
481
+
482
+ def cmd_scan(config: dict, args):
483
+ """扫描变动"""
484
+ results = scan(
485
+ config,
486
+ filter_kb=args.kb,
487
+ filter_module=args.module,
488
+ force=args.force
489
+ )
490
+
491
+ # 保存报告
492
+ report = {
493
+ "timestamp": datetime.now().isoformat(),
494
+ "results": results
495
+ }
496
+ save_report(config, report)
497
+
498
+ # 统计
499
+ total_changed = 0
500
+ total_unchanged = 0
501
+ total_errors = 0
502
+
503
+ for kb_results in results.values():
504
+ if isinstance(kb_results, dict) and "error" not in kb_results:
505
+ for module_result in kb_results.values():
506
+ if "error" in module_result:
507
+ total_errors += 1
508
+ elif module_result.get("status") == "changed":
509
+ total_changed += 1
510
+ elif module_result.get("status") == "unchanged":
511
+ total_unchanged += 1
512
+
513
+ print(f"\n总结:")
514
+ print(f" 变动: {total_changed} 个模块")
515
+ print(f" 无变动: {total_unchanged} 个模块")
516
+ if total_errors > 0:
517
+ print(f" 错误: {total_errors} 个模块")
518
+
519
+ # 结构化状态输出
520
+ print(json.dumps({"status": "ok", "changed": total_changed, "unchanged": total_unchanged, "errors": total_errors}, ensure_ascii=False), file=sys.stderr)
521
+
522
+
523
+ def cmd_update_state(config: dict, args):
524
+ """更新状态文件中的基线 commit"""
525
+ if not args.kb or not args.module or not args.commit:
526
+ print("错误: --update-state 需要 --kb <知识库> --module <模块> <commit>", file=sys.stderr)
527
+ sys.exit(1)
528
+
529
+ state = load_state(config)
530
+
531
+ if args.kb not in state:
532
+ state[args.kb] = {}
533
+
534
+ state[args.kb][args.module] = args.commit
535
+ save_state(config, state)
536
+
537
+ print(f"已更新: {args.kb}/{args.module} -> {args.commit[:8]}")
538
+
539
+
540
+ def cmd_batch_update(config: dict):
541
+ """批量更新基线(将所有模块更新到最新 commit)"""
542
+ print("批量更新基线...")
543
+
544
+ state = load_state(config)
545
+ kbs = config.get("knowledge_bases", {})
546
+ for kb_name, kb_config in kbs.items():
547
+ print(f"\n知识库: {kb_name}")
548
+
549
+ # 从 kb 配置读取缓存目录(与 scan() 一致)
550
+ source = kb_config.get("source", {})
551
+ cache_dir = Path(source.get("cache_dir", "./.source-cache"))
552
+ if not cache_dir.is_absolute():
553
+ cache_dir = PROJECT_ROOT / cache_dir
554
+
555
+ modules = get_modules(kb_config)
556
+
557
+ if kb_name not in state:
558
+ state[kb_name] = {}
559
+
560
+ for module_config in modules:
561
+ module_name = module_config["name"]
562
+ branch = module_config.get("branch", "main")
563
+
564
+ print(f" 模块: {module_name}")
565
+
566
+ try:
567
+ repo_path = ensure_repo(
568
+ url=module_config.get("url"),
569
+ local=module_config.get("local"),
570
+ cache_dir=cache_dir,
571
+ module_name=module_name,
572
+ branch=branch
573
+ )
574
+
575
+ ref = f"origin/{branch}"
576
+ commit = get_head_commit(repo_path, ref)
577
+
578
+ if commit:
579
+ old_commit = state[kb_name].get(module_name)
580
+ state[kb_name][module_name] = commit
581
+
582
+ if old_commit:
583
+ print(f" 更新: {old_commit[:8]} -> {commit[:8]}")
584
+ else:
585
+ print(f" 初始化: {commit[:8]}")
586
+ else:
587
+ print(f" 错误: 无法获取 commit")
588
+
589
+ except Exception as e:
590
+ print(f" 错误: {e}")
591
+
592
+ save_state(config, state)
593
+ print(f"\n状态已保存: {get_state_file(config)}")
594
+
595
+
596
+ # ============================================================================
597
+ # 主函数
598
+ # ============================================================================
599
+
600
+ def main():
601
+ parser = argparse.ArgumentParser(
602
+ description="扫描仓库变动",
603
+ formatter_class=argparse.RawDescriptionHelpFormatter,
604
+ epilog=__doc__
605
+ )
606
+
607
+ parser.add_argument(
608
+ "--config",
609
+ type=str,
610
+ help="指定 kb-project.yaml 路径(可选,默认自动查找)"
611
+ )
612
+
613
+ parser.add_argument(
614
+ "--init",
615
+ action="store_true",
616
+ help="初始化状态文件"
617
+ )
618
+
619
+ parser.add_argument(
620
+ "--kb",
621
+ type=str,
622
+ help="只扫描指定知识库"
623
+ )
624
+
625
+ parser.add_argument(
626
+ "--module",
627
+ type=str,
628
+ help="只扫描指定模块"
629
+ )
630
+
631
+ parser.add_argument(
632
+ "--force",
633
+ action="store_true",
634
+ help="强制扫描(忽略状态文件)"
635
+ )
636
+
637
+ parser.add_argument( "--update-state",
638
+ action="store_true",
639
+ help="更新状态文件中的基线 commit"
640
+ )
641
+
642
+ parser.add_argument(
643
+ "--batch-update",
644
+ action="store_true",
645
+ help="批量更新基线(将所有模块更新到最新 commit)"
646
+ )
647
+
648
+ parser.add_argument(
649
+ "--commit",
650
+ type=str,
651
+ help="commit hash(用于 --update-state)"
652
+ )
653
+
654
+ args = parser.parse_args()
655
+
656
+ # 加载配置
657
+ try:
658
+ if args.config:
659
+ config = load_config(Path(args.config))
660
+ else:
661
+ config = load_config()
662
+ except Exception as e:
663
+ print(f"错误: 加载配置失败: {e}", file=sys.stderr)
664
+ sys.exit(1)
665
+
666
+ # 执行命令
667
+ try:
668
+ if args.init:
669
+ cmd_init(config)
670
+ elif args.update_state:
671
+ cmd_update_state(config, args)
672
+ elif args.batch_update:
673
+ cmd_batch_update(config)
674
+ else:
675
+ cmd_scan(config, args)
676
+ except KeyboardInterrupt:
677
+ print("\n\n已取消", file=sys.stderr)
678
+ sys.exit(130)
679
+ except Exception as e:
680
+ print(f"\n错误: {e}", file=sys.stderr)
681
+ import traceback
682
+ traceback.print_exc()
683
+ sys.exit(1)
684
+
685
+
686
+ if __name__ == "__main__":
687
+ main()