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,237 @@
1
+ """Sync finalize steps — post-update validation, baseline, and repair.
2
+
3
+ Steps:
4
+ - SharedDocsCascadeStep: update _shared/ docs when cross-module info changes
5
+ - UpdateBaselineStep: update baseline ONLY after validation passes
6
+ - AnchorFixStep: repair broken cross-document links
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ from pathlib import Path
14
+
15
+ from core.interfaces import Step, StepResult, PipelineContext
16
+ from core.git import get_head_commit
17
+ from engine.pipeline import register_step
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @register_step
23
+ class SharedDocsCascadeStep(Step):
24
+ """Update _shared/ documents when module changes affect cross-module info.
25
+
26
+ Triggers:
27
+ - New/modified @FeignClient -> _shared/cross-module-calls.md
28
+ - New/modified error codes -> _shared/error-codes.md
29
+ - New/modified @TableName -> _shared/database-ddl.md
30
+ """
31
+
32
+ default_name = "shared-docs-cascade"
33
+
34
+ def __init__(self):
35
+ super().__init__("shared-docs-cascade")
36
+
37
+ def run(self, ctx: PipelineContext) -> StepResult:
38
+ affected_docs = ctx.state.get("affected_docs", {})
39
+ if not affected_docs:
40
+ return StepResult(status="skipped", message="No affected docs")
41
+
42
+ shared_dir = ctx.knowledge_dir / "_shared"
43
+ if not shared_dir.is_dir():
44
+ return StepResult(status="skipped", message="No _shared/ directory")
45
+
46
+ triggers = _detect_shared_doc_triggers(affected_docs)
47
+ if not triggers:
48
+ return StepResult(status="skipped", message="No shared doc impact")
49
+
50
+ logger.info("[shared-docs-cascade] Detected triggers: %s", triggers)
51
+ return StepResult(
52
+ status="ok",
53
+ message=f"Detected {len(triggers)} shared doc triggers (update deferred)",
54
+ details={"triggers": triggers},
55
+ )
56
+
57
+
58
+ @register_step
59
+ class UpdateBaselineStep(Step):
60
+ """Update sync baseline ONLY after all validations pass.
61
+
62
+ Preconditions:
63
+ - Not too many failures (fail ratio < 50%)
64
+ - No fatal errors in prior steps
65
+ """
66
+
67
+ default_name = "update-baseline"
68
+
69
+ def __init__(self):
70
+ super().__init__("update-baseline")
71
+
72
+ def run(self, ctx: PipelineContext) -> StepResult:
73
+ failed_sections = ctx.state.get("failed_sections", [])
74
+ updated_sections = ctx.state.get("updated_sections", [])
75
+ total = len(updated_sections) + len(failed_sections)
76
+
77
+ if total > 0 and len(failed_sections) > 0:
78
+ fail_ratio = len(failed_sections) / total
79
+ if fail_ratio > 0.5:
80
+ return StepResult(
81
+ status="skipped",
82
+ message=f"Too many failures ({len(failed_sections)}/{total}), baseline not updated",
83
+ )
84
+
85
+ if ctx.state.get("dry_run", False):
86
+ _write_dry_run_report(ctx)
87
+ return StepResult(status="skipped", message="Dry-run: baseline not updated")
88
+
89
+ _update_baseline(ctx)
90
+ return StepResult(status="ok", message="Baseline updated")
91
+
92
+
93
+ @register_step
94
+ class AnchorFixStep(Step):
95
+ """Repair broken cross-document anchor links after document updates."""
96
+
97
+ default_name = "anchor-fix"
98
+
99
+ def __init__(self):
100
+ super().__init__("anchor-fix")
101
+
102
+ def run(self, ctx: PipelineContext) -> StepResult:
103
+ from core.skeleton.anchor_fix import fix_anchors
104
+
105
+ updated_sections = ctx.state.get("updated_sections", [])
106
+ if not updated_sections:
107
+ return StepResult(status="skipped", message="No updates to check")
108
+
109
+ module_names = {s["module"] for s in updated_sections}
110
+ total_fixed = 0
111
+ total_degraded = 0
112
+
113
+ for module_name in module_names:
114
+ module_dir = ctx.knowledge_dir / module_name
115
+ if not module_dir.is_dir():
116
+ continue
117
+
118
+ result = fix_anchors(module_dir)
119
+ total_fixed += result.links_fixed
120
+ total_degraded += result.links_degraded
121
+
122
+ if total_fixed or total_degraded:
123
+ return StepResult(
124
+ status="ok",
125
+ message=f"Anchor fix: {total_fixed} fixed, {total_degraded} degraded",
126
+ )
127
+ return StepResult(status="ok", message="All links valid")
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # Helpers
132
+ # ---------------------------------------------------------------------------
133
+
134
+
135
+ def _load_baseline(ctx: PipelineContext) -> dict[str, str]:
136
+ """Load sync baseline (module -> commit hash)."""
137
+ state_dir = ctx.knowledge_dir.parent / ".source-kb"
138
+ state_file = state_dir / "sync-state.json"
139
+ if not state_file.exists():
140
+ return {}
141
+ try:
142
+ all_state = json.loads(state_file.read_text(encoding="utf-8"))
143
+ return all_state.get(ctx.kb_name, {})
144
+ except (json.JSONDecodeError, OSError):
145
+ return {}
146
+
147
+
148
+ def _update_baseline(ctx: PipelineContext) -> None:
149
+ """Update sync baseline with current HEAD commits."""
150
+ module_repos: dict[str, Path] = ctx.state.get("module_repos", {})
151
+ if not module_repos:
152
+ return
153
+
154
+ state_dir = ctx.knowledge_dir.parent / ".source-kb"
155
+ state_dir.mkdir(parents=True, exist_ok=True)
156
+ state_file = state_dir / "sync-state.json"
157
+
158
+ all_state: dict = {}
159
+ if state_file.exists():
160
+ try:
161
+ all_state = json.loads(state_file.read_text(encoding="utf-8"))
162
+ except (json.JSONDecodeError, OSError):
163
+ pass
164
+
165
+ kb_state = all_state.setdefault(ctx.kb_name, {})
166
+ for name, repo_path in module_repos.items():
167
+ branch = ctx.state.get("module_branches", {}).get(name, "main")
168
+ commit = get_head_commit(repo_path, ref=f"origin/{branch}")
169
+ if commit:
170
+ kb_state[name] = commit
171
+
172
+ state_file.write_text(
173
+ json.dumps(all_state, ensure_ascii=False, indent=2), encoding="utf-8"
174
+ )
175
+
176
+
177
+ def _classify_changes(changed_files: list[str], preset: dict) -> dict[str, list[str]]:
178
+ """Classify changed files into affected doc types."""
179
+ from core.preset_classify import classify_file
180
+
181
+ classification: dict[str, list[str]] = {}
182
+ for fpath in changed_files:
183
+ categories = classify_file(preset, fpath)
184
+ if categories:
185
+ for cat in categories:
186
+ classification.setdefault(cat, []).append(fpath)
187
+ else:
188
+ fallback_cat = "source"
189
+ doc_types = preset.get("doc_types", {})
190
+ for dt_key, dt_config in doc_types.items():
191
+ if isinstance(dt_config, dict) and not dt_config.get("conditional", True):
192
+ fallback_cat = dt_key
193
+ break
194
+ classification.setdefault(fallback_cat, []).append(fpath)
195
+
196
+ return classification
197
+
198
+
199
+ def _write_dry_run_report(ctx: PipelineContext) -> None:
200
+ """Write dry-run report with proposed changes."""
201
+ changes = ctx.state.get("dry_run_changes", [])
202
+ updated = ctx.state.get("updated_sections", [])
203
+
204
+ report = {
205
+ "operation": "kb-sync",
206
+ "mode": "dry-run",
207
+ "proposed_changes": changes + [
208
+ {"doc_path": s["doc"], "section": s["heading"],
209
+ "action": s["action"], "module": s["module"]}
210
+ for s in updated
211
+ ],
212
+ "summary": {
213
+ "total_changes": len(changes) + len(updated),
214
+ },
215
+ }
216
+
217
+ report_path = ctx.knowledge_dir / ".dry-run-report.json"
218
+ report_path.write_text(
219
+ json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
220
+ )
221
+ logger.info("[dry-run] Report written: %s", report_path)
222
+
223
+
224
+ def _detect_shared_doc_triggers(affected_docs: dict[str, list[str]]) -> list[str]:
225
+ """Detect if changes affect shared documents."""
226
+ triggers: list[str] = []
227
+
228
+ if any(cat in affected_docs for cat in {"feign", "feign-external"}):
229
+ triggers.append("cross-module-calls")
230
+
231
+ if "error-handling" in affected_docs:
232
+ triggers.append("error-codes")
233
+
234
+ if "model" in affected_docs:
235
+ triggers.append("database-ddl")
236
+
237
+ return triggers
@@ -0,0 +1,341 @@
1
+ """Sync update step — LLM-powered incremental document section updates.
2
+
3
+ Steps:
4
+ - IncrementalUpdateStep: LLM call per affected section, or delegated manifest
5
+
6
+ Requirements: Req 11, 15, 19, 24
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ from pathlib import Path
14
+ from typing import Any, TYPE_CHECKING
15
+
16
+ from core.interfaces import Step, StepResult, PipelineContext, LlmRequest
17
+ from core.utils import validate_path_within_bounds
18
+ from engine.pipeline import register_step
19
+
20
+ if TYPE_CHECKING:
21
+ from core.skeleton.impact import SectionImpact
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ @register_step
27
+ class IncrementalUpdateStep(Step):
28
+ """LLM-powered incremental document section updates.
29
+
30
+ Modes:
31
+ - CLI (api): Impact analysis -> LLM call -> validate -> write
32
+ - Delegated: Generate .sync-manifest.json for Agent dispatch
33
+ """
34
+
35
+ default_name = "incremental-update"
36
+
37
+ def __init__(self):
38
+ super().__init__("incremental-update", checkpoint="incremental-update")
39
+
40
+ def run(self, ctx: PipelineContext) -> StepResult:
41
+ from core.preset import load_preset
42
+ from core.skeleton.impact import analyze_impact
43
+ from core.docs.section_updater import replace_section, append_section
44
+ from core.prompt.response_parser import validate_sync_response
45
+ from engine.strategies import create_strategy
46
+ from engine.pipeline.steps.generate import _make_config_obj
47
+ from engine.pipeline.steps.sync_finalize import _classify_changes
48
+
49
+ changed_files = ctx.state.get("changed_files", [])
50
+ if not changed_files:
51
+ return StepResult(status="skipped", message="No changes to process")
52
+
53
+ preset_name = ctx.kb_config.get("preset", "generic")
54
+ preset = load_preset(preset_name)
55
+ config_obj = _make_config_obj(ctx)
56
+
57
+ change_details: dict[str, list[dict]] = ctx.state.get("change_details", {})
58
+
59
+ # Create strategy — delegated mode is handled via response.status, not upfront check
60
+ strategy = create_strategy(config_obj)
61
+ dry_run = ctx.state.get("dry_run", False)
62
+
63
+ total_updated = 0
64
+ total_failed = 0
65
+ all_updated_sections: list[dict] = []
66
+ all_failed_sections: list[dict] = []
67
+
68
+ for module_name, changes in change_details.items():
69
+ module_dir = ctx.knowledge_dir / module_name
70
+ source_cache = ctx.cache_dir / module_name
71
+
72
+ if not module_dir.is_dir():
73
+ continue
74
+
75
+ module_files = [c["file"] for c in changes]
76
+
77
+ plan = analyze_impact(
78
+ changed_files=module_files,
79
+ module_dir=module_dir,
80
+ preset=preset,
81
+ source_cache=source_cache,
82
+ change_details=changes,
83
+ )
84
+
85
+ if not plan.sections:
86
+ logger.info("[sync] %s: no sections affected", module_name)
87
+ continue
88
+
89
+ for impact in plan.sections:
90
+ doc_path = module_dir / impact.doc_path
91
+ if not validate_path_within_bounds(doc_path, module_dir):
92
+ logger.warning("[sync] Path traversal blocked: %s", doc_path)
93
+ total_failed += 1
94
+ continue
95
+ if not doc_path.exists() and impact.change_type != "create":
96
+ all_failed_sections.append({
97
+ "module": module_name, "doc": impact.doc_path,
98
+ "heading": impact.section_heading, "reason": "doc not found",
99
+ })
100
+ total_failed += 1
101
+ continue
102
+
103
+ old_content = ""
104
+ if doc_path.exists() and impact.change_type == "update":
105
+ old_content = _read_section_content(doc_path, impact.section_heading)
106
+
107
+ system_prompt = _build_sync_system_prompt(impact.doc_path)
108
+ user_prompt = _build_sync_user_prompt(
109
+ impact, old_content, module_files, source_cache
110
+ )
111
+
112
+ if dry_run:
113
+ ctx.state.setdefault("dry_run_changes", []).append({
114
+ "module": module_name,
115
+ "doc_path": impact.doc_path,
116
+ "section": impact.section_heading,
117
+ "action": impact.change_type,
118
+ "affected_files": impact.affected_files,
119
+ })
120
+ total_updated += 1
121
+ continue
122
+
123
+ request = LlmRequest(system=system_prompt, user=user_prompt)
124
+ try:
125
+ response = strategy.call(request)
126
+ except Exception as e:
127
+ logger.error("[sync] LLM call failed for %s/%s: %s",
128
+ module_name, impact.section_heading, e)
129
+ all_failed_sections.append({
130
+ "module": module_name, "doc": impact.doc_path,
131
+ "heading": impact.section_heading, "reason": str(e),
132
+ })
133
+ total_failed += 1
134
+ continue
135
+
136
+ if response.status == "delegated":
137
+ # Accumulate for manifest generation
138
+ ctx.state.setdefault("_delegated_sections", []).append({
139
+ "module": module_name,
140
+ "doc_path": impact.doc_path,
141
+ "section_heading": impact.section_heading,
142
+ "action": impact.change_type,
143
+ "affected_files": impact.affected_files,
144
+ "confidence": impact.confidence,
145
+ })
146
+ total_updated += 1
147
+ continue
148
+
149
+ if response.status != "done":
150
+ all_failed_sections.append({
151
+ "module": module_name, "doc": impact.doc_path,
152
+ "heading": impact.section_heading,
153
+ "reason": f"LLM status: {response.status}",
154
+ })
155
+ total_failed += 1
156
+ continue
157
+
158
+ is_valid, content = validate_sync_response(response.content, old_content)
159
+ if not is_valid:
160
+ logger.warning("[sync] Invalid response for %s/%s: %s",
161
+ module_name, impact.section_heading, content)
162
+ _mark_unverified(doc_path, impact.section_heading)
163
+ all_failed_sections.append({
164
+ "module": module_name, "doc": impact.doc_path,
165
+ "heading": impact.section_heading, "reason": content,
166
+ })
167
+ total_failed += 1
168
+ continue
169
+
170
+ if impact.change_type == "update":
171
+ replace_section(doc_path, impact.section_heading, content)
172
+ elif impact.change_type == "create":
173
+ append_section(doc_path, impact.section_heading, content)
174
+
175
+ all_updated_sections.append({
176
+ "module": module_name, "doc": impact.doc_path,
177
+ "heading": impact.section_heading, "action": impact.change_type,
178
+ })
179
+ total_updated += 1
180
+
181
+ ctx.state["updated_sections"] = all_updated_sections
182
+ ctx.state["failed_sections"] = all_failed_sections
183
+
184
+ affected_docs = _classify_changes(changed_files, preset)
185
+ ctx.state["affected_docs"] = affected_docs
186
+
187
+ # If delegated mode accumulated sections, write manifest and return delegated
188
+ delegated_sections = ctx.state.pop("_delegated_sections", [])
189
+ if delegated_sections:
190
+ return self._write_manifest(ctx, delegated_sections, change_details)
191
+
192
+ if total_updated == 0 and total_failed == 0:
193
+ return StepResult(status="skipped", message="No sections affected")
194
+
195
+ return StepResult(
196
+ status="ok",
197
+ message=f"Updated {total_updated} sections, {total_failed} failed",
198
+ details={"updated": total_updated, "failed": total_failed},
199
+ )
200
+
201
+ def _write_manifest(
202
+ self, ctx: PipelineContext, delegated_sections: list[dict],
203
+ change_details: dict[str, list[dict]],
204
+ ) -> StepResult:
205
+ """Write sync manifest from accumulated delegated responses."""
206
+ manifest = {"operation": "kb-sync", "modules": []}
207
+
208
+ # Group sections by module
209
+ by_module: dict[str, list[dict]] = {}
210
+ for s in delegated_sections:
211
+ by_module.setdefault(s["module"], []).append(s)
212
+
213
+ for module_name, sections in by_module.items():
214
+ module_files = [c["file"] for c in change_details.get(module_name, [])]
215
+ module_entry = {
216
+ "name": module_name,
217
+ "changed_files": module_files,
218
+ "affected_sections": [
219
+ {
220
+ "doc_path": s["doc_path"],
221
+ "section_heading": s["section_heading"],
222
+ "action": s["action"],
223
+ "affected_files": s["affected_files"],
224
+ "confidence": s.get("confidence", 1.0),
225
+ }
226
+ for s in sections
227
+ ],
228
+ }
229
+ manifest["modules"].append(module_entry)
230
+
231
+ manifest_path = ctx.knowledge_dir / ".sync-manifest.json"
232
+ manifest_path.write_text(
233
+ json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8"
234
+ )
235
+
236
+ total_sections = sum(len(m["affected_sections"]) for m in manifest["modules"])
237
+ return StepResult(
238
+ status="delegated",
239
+ message=f"Manifest: {total_sections} sections in {len(manifest['modules'])} modules",
240
+ details={"manifest_path": str(manifest_path), "sections": total_sections},
241
+ )
242
+
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # Helpers
246
+ # ---------------------------------------------------------------------------
247
+
248
+
249
+ def _read_section_content(doc_path: Path, heading: str) -> str:
250
+ """Read the content of a specific section from a document."""
251
+ from core.docs.section_updater import find_section_boundaries
252
+
253
+ try:
254
+ content = doc_path.read_text(encoding="utf-8", errors="replace")
255
+ except OSError:
256
+ return ""
257
+
258
+ lines = content.split("\n")
259
+ if lines and lines[-1] == "":
260
+ lines = lines[:-1]
261
+
262
+ bounds = find_section_boundaries(lines, heading)
263
+ if bounds is None:
264
+ return ""
265
+
266
+ start, end = bounds
267
+ section_lines = lines[start + 1:end]
268
+ return "\n".join(section_lines).strip()
269
+
270
+
271
+ def _mark_unverified(doc_path: Path, heading: str) -> None:
272
+ """Mark a section as unverified by prepending a warning marker."""
273
+ from core.docs.section_updater import find_section_boundaries
274
+
275
+ try:
276
+ content = doc_path.read_text(encoding="utf-8")
277
+ except OSError:
278
+ return
279
+
280
+ lines = content.split("\n")
281
+ if lines and lines[-1] == "":
282
+ lines = lines[:-1]
283
+
284
+ bounds = find_section_boundaries(lines, heading)
285
+ if bounds is None:
286
+ return
287
+
288
+ start, end = bounds
289
+ for i in range(start + 1, end):
290
+ if lines[i].strip():
291
+ if not lines[i].startswith("[UNVERIFIED]"):
292
+ lines[i] = f"[UNVERIFIED] {lines[i]}"
293
+ break
294
+
295
+ doc_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
296
+
297
+
298
+ def _build_sync_system_prompt(doc_type: str) -> str:
299
+ """Build system prompt for sync section update."""
300
+ from core.preset import find_preset_template
301
+
302
+ template_path = find_preset_template("java-spring", "sync-system.md")
303
+ if template_path and template_path.exists():
304
+ try:
305
+ return template_path.read_text(encoding="utf-8").strip()
306
+ except OSError:
307
+ pass
308
+ return (
309
+ "You are a knowledge base maintainer. Update the specific document section "
310
+ "based on source code changes.\n\n"
311
+ "Rules:\n"
312
+ "- Only rewrite the affected section content, maintain the style of the rest\n"
313
+ "- Describe from a business perspective, not code translation\n"
314
+ "- Output section body only (without the heading line)\n"
315
+ "- Do not fabricate; mark uncertain content with [UNVERIFIED]"
316
+ )
317
+
318
+
319
+ def _build_sync_user_prompt(
320
+ impact: SectionImpact, old_content: str, changed_files: list[str], source_cache: Path
321
+ ) -> str:
322
+ """Build user prompt for sync section update."""
323
+ parts = [
324
+ f"Document: {impact.doc_path}",
325
+ f"Section: {impact.section_heading}",
326
+ f"Change type: {impact.change_type}",
327
+ "",
328
+ "Current section content:",
329
+ old_content if old_content else "(empty)",
330
+ "",
331
+ "Changed files:",
332
+ ]
333
+ for f in impact.affected_files[:10]:
334
+ parts.append(f" - {f}")
335
+ if len(impact.affected_files) > 10:
336
+ parts.append(f" ... and {len(impact.affected_files) - 10} more files")
337
+
338
+ parts.append("")
339
+ parts.append("Rewrite this section based on the source code changes.")
340
+
341
+ return "\n".join(parts)
engine/pipelines.py ADDED
@@ -0,0 +1,91 @@
1
+ """Pipeline builders — compose steps into named pipelines.
2
+
3
+ Usage:
4
+ from engine.pipelines import build_init_pipeline, build_sync_pipeline, build_audit_pipeline
5
+
6
+ pipeline = build_init_pipeline()
7
+ result = pipeline.execute(ctx)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from engine.pipeline import Pipeline
13
+ from engine.pipeline.steps.lock import AcquireLockStep, ReleaseLockStep
14
+ from engine.pipeline.steps.source import FetchSourceStep, BuildSourceStep, ResolveDepsStep
15
+ from engine.pipeline.steps.skeleton import ExtractSkeletonStep, ExtractFileListStep, ClassifyStep
16
+ from engine.pipeline.steps.pregenerate import PregenerateMetadataStep, DispatchPlanStep
17
+ from engine.pipeline.steps.preflight import PreflightCheckStep
18
+ from engine.pipeline.steps.generate import GenerateDocsStep
19
+ from engine.pipeline.steps.quality import ValidateStep, CoveragePreCheckStep
20
+ from engine.pipeline.steps.finalize import (
21
+ MergeShardsStep, DeduplicateStep, RebuildIndexStep,
22
+ CleanProgressStep, SharedDocsStep,
23
+ )
24
+ from engine.pipeline.steps.sync import (
25
+ UpdateCacheStep, DetectChangesStep,
26
+ )
27
+ from engine.pipeline.steps.sync_update import IncrementalUpdateStep
28
+ from engine.pipeline.steps.sync_finalize import (
29
+ SharedDocsCascadeStep, UpdateBaselineStep, AnchorFixStep,
30
+ )
31
+ from engine.pipeline.steps.audit import FetchAndScaleStep, AuditDocsStep
32
+ from engine.pipeline.steps.audit_apply import ApplyFixesStep, GenerateReportStep
33
+
34
+
35
+ def build_init_pipeline() -> Pipeline:
36
+ """Full kb-init pipeline."""
37
+ return Pipeline("kb-init", [
38
+ AcquireLockStep(timeout_minutes=120),
39
+ FetchSourceStep(),
40
+ BuildSourceStep(),
41
+ ResolveDepsStep(),
42
+ ExtractSkeletonStep(),
43
+ ExtractFileListStep(),
44
+ ClassifyStep(),
45
+ CoveragePreCheckStep(),
46
+ PregenerateMetadataStep(),
47
+ DispatchPlanStep(),
48
+ PreflightCheckStep(),
49
+ GenerateDocsStep(),
50
+ MergeShardsStep(),
51
+ DeduplicateStep(),
52
+ ValidateStep(),
53
+ SharedDocsStep(),
54
+ CleanProgressStep(),
55
+ RebuildIndexStep(),
56
+ ReleaseLockStep(),
57
+ ])
58
+
59
+
60
+ def build_sync_pipeline() -> Pipeline:
61
+ """Incremental kb-sync pipeline."""
62
+ return Pipeline("kb-sync", [
63
+ AcquireLockStep(timeout_minutes=30),
64
+ UpdateCacheStep(),
65
+ BuildSourceStep(),
66
+ DetectChangesStep(),
67
+ ExtractSkeletonStep(),
68
+ ExtractFileListStep(),
69
+ IncrementalUpdateStep(),
70
+ SharedDocsCascadeStep(),
71
+ ValidateStep(),
72
+ UpdateBaselineStep(),
73
+ AnchorFixStep(),
74
+ RebuildIndexStep(),
75
+ ReleaseLockStep(),
76
+ ])
77
+
78
+
79
+ def build_audit_pipeline() -> Pipeline:
80
+ """kb-audit pipeline."""
81
+ return Pipeline("kb-audit", [
82
+ AcquireLockStep(timeout_minutes=30),
83
+ FetchAndScaleStep(),
84
+ AuditDocsStep(),
85
+ ApplyFixesStep(),
86
+ AnchorFixStep(),
87
+ ValidateStep(),
88
+ RebuildIndexStep(),
89
+ GenerateReportStep(),
90
+ ReleaseLockStep(),
91
+ ])