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,130 @@
1
+ """Pre-generation steps — global metadata and dispatch plan.
2
+
3
+ Steps that run before generate-docs to prepare shared context:
4
+ - PregenerateMetadataStep: generates .meta/global-metadata.md
5
+ - DispatchPlanStep: computes and writes .meta/dispatch-plan.md + dispatch-tasks.json
6
+
7
+ These are required for prompt rendering (global_metadata variable) and
8
+ for agent-mode task dispatching.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ from pathlib import Path
16
+
17
+ from core.interfaces import Step, StepResult, PipelineContext
18
+ from engine.pipeline import register_step
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ @register_step
24
+ class PregenerateMetadataStep(Step):
25
+ """Generate global-metadata.md for each module."""
26
+
27
+ default_name = "pregenerate-metadata"
28
+
29
+ def __init__(self):
30
+ super().__init__("pregenerate-metadata")
31
+
32
+ def run(self, ctx: PipelineContext) -> StepResult:
33
+ from core.skeleton.metadata import pregenerate
34
+
35
+ module_repos: dict[str, Path] = ctx.state.get("module_repos", {})
36
+ generated = []
37
+
38
+ for name in module_repos:
39
+ module_dir = ctx.knowledge_dir / name
40
+ try:
41
+ path = pregenerate(module_dir, module_name=name)
42
+ generated.append(name)
43
+ logger.info("[pregenerate-metadata] %s: %s", name, path)
44
+ except Exception as e:
45
+ logger.warning("[pregenerate-metadata] %s: %s", name, e)
46
+
47
+ if not generated:
48
+ return StepResult(status="skipped", message="No metadata generated")
49
+ return StepResult(status="ok", message=f"Generated metadata for {len(generated)} modules")
50
+
51
+
52
+ @register_step
53
+ class DispatchPlanStep(Step):
54
+ """Compute and write dispatch-plan.md + dispatch-tasks.json."""
55
+
56
+ default_name = "dispatch-plan"
57
+
58
+ def __init__(self):
59
+ super().__init__("dispatch-plan")
60
+
61
+ def run(self, ctx: PipelineContext) -> StepResult:
62
+ from core.preset import load_preset, get_doc_type_mapping
63
+ from core.skeleton.dispatch import compute_dispatch_plan
64
+ from core.skeleton.dispatch_render import render_markdown
65
+
66
+ preset_name = ctx.kb_config.get("preset", "generic")
67
+ preset = load_preset(preset_name)
68
+
69
+ module_repos: dict[str, Path] = ctx.state.get("module_repos", {})
70
+ plans_written = []
71
+
72
+ for name, repo_path in module_repos.items():
73
+ module_dir = ctx.knowledge_dir / name
74
+ source_cache = ctx.cache_dir / name
75
+
76
+ # Determine module type from config
77
+ module_type = _get_module_type(ctx, name)
78
+
79
+ try:
80
+ plan = compute_dispatch_plan(
81
+ preset=preset,
82
+ module_dir=module_dir,
83
+ source_cache=source_cache,
84
+ mode="output-only",
85
+ module_name=name,
86
+ module_type=module_type,
87
+ )
88
+ except Exception as e:
89
+ logger.warning("[dispatch-plan] %s: %s", name, e)
90
+ continue
91
+
92
+ # Write dispatch-plan.md
93
+ meta_dir = module_dir / ".meta"
94
+ meta_dir.mkdir(parents=True, exist_ok=True)
95
+
96
+ plan_md = render_markdown(plan, mode="output-only")
97
+ (meta_dir / "dispatch-plan.md").write_text(plan_md, encoding="utf-8")
98
+
99
+ # Write dispatch-tasks.json
100
+ from core.skeleton.dispatch_render import plan_to_tasks, write_shard_files
101
+ tasks = plan_to_tasks(
102
+ plan=plan, kb_name=ctx.kb_name, preset_name=preset_name,
103
+ preset=preset, knowledge_dir=ctx.knowledge_dir, mode="output-only",
104
+ )
105
+ (meta_dir / "dispatch-tasks.json").write_text(
106
+ json.dumps(tasks, ensure_ascii=False, indent=2), encoding="utf-8"
107
+ )
108
+
109
+ # Write shard file lists for split entries
110
+ write_shard_files(plan, module_dir)
111
+
112
+ plans_written.append(name)
113
+ logger.info("[dispatch-plan] %s: %d entries", name, len(plan.entries))
114
+
115
+ if not plans_written:
116
+ return StepResult(status="skipped", message="No dispatch plans generated")
117
+ return StepResult(
118
+ status="ok",
119
+ message=f"Dispatch plans for {len(plans_written)} modules",
120
+ )
121
+
122
+
123
+ def _get_module_type(ctx: PipelineContext, module_name: str) -> str:
124
+ """Get module type from kb config."""
125
+ source = ctx.kb_config.get("source", {})
126
+ repos = source.get("repos", source.get("modules", []))
127
+ for repo in repos:
128
+ if repo.get("name") == module_name:
129
+ return repo.get("type", "service")
130
+ return "service"
@@ -0,0 +1,81 @@
1
+ """Quality validation steps — coverage, sampling, links, duplicates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from core.interfaces import Step, StepResult, PipelineContext
8
+ from engine.pipeline import register_step
9
+
10
+
11
+ @register_step
12
+ class ValidateStep(Step):
13
+ """Run all registered validators on generated documents."""
14
+
15
+ default_name = "validate"
16
+
17
+ def __init__(self):
18
+ super().__init__("validate", checkpoint="cp5")
19
+
20
+ def run(self, ctx: PipelineContext) -> StepResult:
21
+ from core.validators import run_all
22
+
23
+ modules = _get_module_dirs(ctx)
24
+ total_errors = 0
25
+ total_warnings = 0
26
+
27
+ for module_dir in modules:
28
+ result = run_all(module_dir)
29
+ total_errors += len(result.errors)
30
+ total_warnings += len(result.warnings)
31
+
32
+ status = "ok" if total_errors == 0 else "failed"
33
+ return StepResult(
34
+ status=status,
35
+ message=f"Validation: {total_errors} errors, {total_warnings} warnings",
36
+ details={"errors": total_errors, "warnings": total_warnings},
37
+ )
38
+
39
+
40
+ @register_step
41
+ class CoveragePreCheckStep(Step):
42
+ """Pre-check method coverage before generation."""
43
+
44
+ default_name = "coverage-precheck"
45
+
46
+ def __init__(self):
47
+ super().__init__("coverage-precheck")
48
+
49
+ def run(self, ctx: PipelineContext) -> StepResult:
50
+ import json
51
+ from core.paths import resolve_skeleton
52
+ from core.skeleton.file_list import load_skeleton
53
+
54
+ module_repos: dict[str, Path] = ctx.state.get("module_repos", {})
55
+ total_methods = 0
56
+ high_methods = 0
57
+
58
+ for name in module_repos:
59
+ module_dir = ctx.knowledge_dir / name
60
+ entries = load_skeleton(module_dir)
61
+ for entry in entries:
62
+ methods = entry.get("methods", [])
63
+ total_methods += len(methods)
64
+ high_methods += sum(1 for m in methods if m.get("complexity") == "high")
65
+
66
+ ctx.state["coverage_precheck"] = {"total_methods": total_methods, "high_methods": high_methods}
67
+ return StepResult(
68
+ status="ok",
69
+ message=f"Pre-check: {total_methods} methods, {high_methods} high-complexity",
70
+ )
71
+
72
+
73
+ def _get_module_dirs(ctx: PipelineContext) -> list[Path]:
74
+ """Get module directories to validate."""
75
+ if ctx.module:
76
+ d = ctx.knowledge_dir / ctx.module
77
+ return [d] if d.is_dir() else []
78
+ return sorted(
79
+ d for d in ctx.knowledge_dir.iterdir()
80
+ if d.is_dir() and not d.name.startswith(".")
81
+ )
@@ -0,0 +1,149 @@
1
+ """Skeleton extraction and file classification steps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from core.interfaces import Step, StepResult, PipelineContext
9
+ from core.paths import ensure_dir
10
+ from engine.pipeline import register_step
11
+
12
+
13
+ @register_step
14
+ class ExtractSkeletonStep(Step):
15
+ """Extract skeleton from source repos using parser chain."""
16
+
17
+ default_name = "extract-skeleton"
18
+
19
+ def __init__(self):
20
+ super().__init__("extract-skeleton", checkpoint="cp3")
21
+
22
+ def run(self, ctx: PipelineContext) -> StepResult:
23
+ from core.skeleton.extract import extract_skeleton
24
+ from core.preset import load_preset
25
+
26
+ preset_name = ctx.kb_config.get("preset", "generic")
27
+ preset = load_preset(preset_name)
28
+ module_repos: dict[str, Path] = ctx.state.get("module_repos", {})
29
+ extracted = []
30
+
31
+ for name, repo_path in module_repos.items():
32
+ module_dir = ctx.knowledge_dir / name
33
+ module_dir.mkdir(parents=True, exist_ok=True)
34
+ ctx.state["current_module_dir"] = module_dir
35
+
36
+ branch = ctx.state.get("module_branches", {}).get(name, "main")
37
+ try:
38
+ entries = extract_skeleton(
39
+ repo_path, preset, ref=f"origin/{branch}",
40
+ output_dir=module_dir, split_by_package=True,
41
+ )
42
+ if entries:
43
+ extracted.append(name)
44
+ except Exception as e:
45
+ print(f" [{name}] skeleton failed: {e}", file=sys.stderr)
46
+
47
+ return StepResult(
48
+ status="ok" if extracted else "skipped",
49
+ message=f"Extracted skeletons for {len(extracted)} modules",
50
+ details={"extracted": extracted},
51
+ )
52
+
53
+
54
+ @register_step
55
+ class ExtractFileListStep(Step):
56
+ """Generate per-doc-type file lists from skeleton."""
57
+
58
+ default_name = "extract-file-list"
59
+
60
+ def __init__(self):
61
+ super().__init__("extract-file-list")
62
+
63
+ def run(self, ctx: PipelineContext) -> StepResult:
64
+ from core.skeleton.file_list import load_skeleton, extract_file_list
65
+ from core.preset import load_preset, get_doc_type_mapping
66
+ from core.paths import file_list_dir, file_list_path
67
+
68
+ preset_name = ctx.kb_config.get("preset", "generic")
69
+ preset = load_preset(preset_name)
70
+ doc_mapping = get_doc_type_mapping(preset)
71
+ module_repos: dict[str, Path] = ctx.state.get("module_repos", {})
72
+
73
+ cache_dir = Path(ctx.kb_config["source"]["cache_dir"])
74
+ if not cache_dir.is_absolute():
75
+ cache_dir = (ctx.project_root / cache_dir).resolve()
76
+
77
+ total_lists = 0
78
+ for name in module_repos:
79
+ module_dir = ctx.knowledge_dir / name
80
+ source_cache = cache_dir / name
81
+ entries = load_skeleton(module_dir)
82
+ if not entries:
83
+ continue
84
+
85
+ fl_dir = file_list_dir(module_dir)
86
+ ensure_dir(fl_dir)
87
+
88
+ for doc_type_key, doc_filename in doc_mapping.items():
89
+ stem = doc_filename.removesuffix(".md")
90
+ files = extract_file_list(entries, preset, doc_type_key, str(source_cache))
91
+ output = file_list_path(module_dir, stem)
92
+ if files:
93
+ output.write_text("\n".join(files) + "\n", encoding="utf-8")
94
+ total_lists += 1
95
+ elif output.exists():
96
+ output.unlink()
97
+
98
+ return StepResult(status="ok", message=f"Extracted {total_lists} file lists")
99
+
100
+
101
+ @register_step
102
+ class ClassifyStep(Step):
103
+ """LLM-assisted classification for uncovered files."""
104
+
105
+ default_name = "classify"
106
+
107
+ def __init__(self):
108
+ super().__init__("classify")
109
+
110
+ def run(self, ctx: PipelineContext) -> StepResult:
111
+ from core.skeleton.file_list import load_skeleton, check_coverage
112
+ from core.skeleton.classify import find_uncovered_files
113
+ from core.preset import load_preset
114
+ from core.paths import file_list_dir
115
+
116
+ preset_name = ctx.kb_config.get("preset", "generic")
117
+ preset = load_preset(preset_name)
118
+ module_repos: dict[str, Path] = ctx.state.get("module_repos", {})
119
+
120
+ total_uncovered = 0
121
+ total_classified = 0
122
+
123
+ for name in module_repos:
124
+ module_dir = ctx.knowledge_dir / name
125
+ entries = load_skeleton(module_dir)
126
+ if not entries:
127
+ continue
128
+
129
+ fl_dir = file_list_dir(module_dir)
130
+ report = check_coverage(entries, preset, fl_dir if fl_dir.is_dir() else None)
131
+
132
+ if report.uncovered_count == 0:
133
+ continue
134
+
135
+ total_uncovered += report.uncovered_count
136
+
137
+ # LLM classification requires strategy — delegate to generate step
138
+ # For now, log uncovered files as warnings
139
+ for f in report.uncovered_files[:10]:
140
+ print(f" [{name}] uncovered: {f}", file=sys.stderr)
141
+
142
+ if total_uncovered == 0:
143
+ return StepResult(status="ok", message="All files covered")
144
+
145
+ return StepResult(
146
+ status="ok",
147
+ message=f"{total_uncovered} uncovered files (LLM classification pending)",
148
+ details={"uncovered": total_uncovered, "classified": total_classified},
149
+ )
@@ -0,0 +1,163 @@
1
+ """Source management steps — fetch, build, resolve dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from core.interfaces import Step, StepResult, PipelineContext
10
+ from core.git import ensure_repo
11
+ from engine.pipeline import register_step
12
+
13
+
14
+ @register_step
15
+ class FetchSourceStep(Step):
16
+ """Clone/fetch source repositories."""
17
+
18
+ default_name = "fetch-source"
19
+
20
+ def __init__(self):
21
+ super().__init__("fetch-source", checkpoint="cp1")
22
+
23
+ def run(self, ctx: PipelineContext) -> StepResult:
24
+ modules = _resolve_modules(ctx)
25
+ fetched, errors = [], []
26
+
27
+ for mod in modules:
28
+ name = mod["name"]
29
+ branch = mod.get("branch", "main")
30
+ url = mod.get("url")
31
+ local = mod.get("local")
32
+ try:
33
+ repo_path = ensure_repo(url, local, ctx.cache_dir, name, branch)
34
+ fetched.append(name)
35
+ ctx.state.setdefault("module_repos", {})[name] = repo_path
36
+ ctx.state.setdefault("module_branches", {})[name] = branch
37
+ except Exception as e:
38
+ errors.append(f"{name}: {e}")
39
+
40
+ if not fetched and errors:
41
+ return StepResult(status="failed", message="; ".join(errors))
42
+ msg = f"Fetched {len(fetched)} modules"
43
+ if errors:
44
+ msg += f" ({len(errors)} errors)"
45
+ return StepResult(status="ok", message=msg, details={"fetched": fetched})
46
+
47
+
48
+ @register_step
49
+ class BuildSourceStep(Step):
50
+ """Execute source build (Maven/Gradle) for jQAssistant support."""
51
+
52
+ default_name = "build-source"
53
+
54
+ def __init__(self):
55
+ super().__init__("build-source")
56
+
57
+ def run(self, ctx: PipelineContext) -> StepResult:
58
+ import yaml
59
+ preset_name = ctx.kb_config.get("preset", "generic")
60
+ preset_path = ctx.project_root / "presets" / preset_name / "preset.yaml"
61
+ if not preset_path.exists():
62
+ return StepResult(status="skipped", message="No preset config")
63
+
64
+ with open(preset_path, encoding="utf-8") as f:
65
+ preset_config = yaml.safe_load(f)
66
+
67
+ build_cfg = preset_config.get("build", {})
68
+ if not build_cfg.get("enabled", False):
69
+ return StepResult(status="skipped", message="Build not enabled")
70
+
71
+ command = build_cfg.get("command", "mvn compile -DskipTests -q")
72
+ timeout = build_cfg.get("timeout", 300)
73
+ module_repos: dict[str, Path] = ctx.state.get("module_repos", {})
74
+ built, warnings = [], []
75
+
76
+ for name, repo_path in module_repos.items():
77
+ build_marker = build_cfg.get("marker_file", "pom.xml")
78
+ if not (repo_path / build_marker).exists():
79
+ continue
80
+ # Skip if already built
81
+ output_dir = build_cfg.get("output_dir", "target/classes")
82
+ if (repo_path / output_dir).is_dir():
83
+ built.append(f"{name} (cached)")
84
+ continue
85
+ try:
86
+ r = subprocess.run(command.split(), capture_output=True, encoding="utf-8",
87
+ timeout=timeout, cwd=str(repo_path))
88
+ if r.returncode == 0:
89
+ built.append(name)
90
+ else:
91
+ warnings.append(f"{name}: build failed (exit {r.returncode})")
92
+ except subprocess.TimeoutExpired:
93
+ warnings.append(f"{name}: build timed out")
94
+
95
+ return StepResult(status="ok", message=f"Built {len(built)} modules",
96
+ details={"built": built, "warnings": warnings})
97
+
98
+
99
+ @register_step
100
+ class ResolveDepsStep(Step):
101
+ """Resolve Maven/Gradle dependencies (jar sources)."""
102
+
103
+ default_name = "resolve-deps"
104
+
105
+ def __init__(self):
106
+ super().__init__("resolve-deps", checkpoint="cp2")
107
+
108
+ def run(self, ctx: PipelineContext) -> StepResult:
109
+ preset = ctx.kb_config.get("preset", "generic")
110
+ dep_script = ctx.project_root / "presets" / preset / "scripts" / "resolve_deps.py"
111
+ if not dep_script.exists():
112
+ return StepResult(status="skipped", message="No dependency resolver")
113
+
114
+ module_repos: dict[str, Path] = ctx.state.get("module_repos", {})
115
+ resolved, errors = [], []
116
+
117
+ for name, repo_path in module_repos.items():
118
+ try:
119
+ r = subprocess.run(
120
+ [sys.executable, "-X", "utf8", str(dep_script), "--repo", str(repo_path),
121
+ "--cache-dir", str(ctx.cache_dir)],
122
+ capture_output=True, encoding="utf-8", timeout=300,
123
+ )
124
+ if r.returncode == 0:
125
+ resolved.append(name)
126
+ else:
127
+ errors.append(f"{name}: exit {r.returncode}")
128
+ except subprocess.TimeoutExpired:
129
+ errors.append(f"{name}: timed out")
130
+
131
+ msg = f"Resolved deps for {len(resolved)} modules"
132
+ if errors:
133
+ msg += f" ({len(errors)} warnings)"
134
+ return StepResult(status="ok", message=msg)
135
+
136
+
137
+ def _resolve_modules(ctx: PipelineContext) -> list[dict]:
138
+ """Get module list from config, filtered by ctx.module and enabled flag."""
139
+ source = ctx.kb_config.get("source", {})
140
+ if source.get("structure") == "multi-repo":
141
+ modules = source.get("repos", [])
142
+ else:
143
+ modules = source.get("modules", [])
144
+
145
+ # Filter disabled modules
146
+ modules = [m for m in modules if m.get("enabled", True) is not False]
147
+
148
+ # Filter excluded modules
149
+ excluded = _parse_excluded_modules(ctx.kb_config)
150
+ if excluded:
151
+ modules = [m for m in modules if m.get("name") not in excluded]
152
+
153
+ if ctx.module:
154
+ modules = [m for m in modules if m.get("name") == ctx.module]
155
+ return modules
156
+
157
+
158
+ def _parse_excluded_modules(kb_config: dict) -> set[str]:
159
+ """Parse excluded_modules from kb config."""
160
+ excluded = kb_config.get("excluded_modules", [])
161
+ if isinstance(excluded, str):
162
+ excluded = [s.strip() for s in excluded.split(",") if s.strip()]
163
+ return set(excluded)
@@ -0,0 +1,117 @@
1
+ """Sync pipeline steps — cache update and change detection.
2
+
3
+ Steps:
4
+ - UpdateCacheStep: git pull on source caches
5
+ - DetectChangesStep: detect changed files via git diff + baseline
6
+
7
+ Requirements: Req 11, 15, 26
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from pathlib import Path
14
+
15
+ from core.interfaces import Step, StepResult, PipelineContext
16
+ from core.git import fetch_with_retry, get_head_commit, diff_files
17
+ from engine.pipeline import register_step
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @register_step
23
+ class UpdateCacheStep(Step):
24
+ """Git pull on source cache directories."""
25
+
26
+ default_name = "update-cache"
27
+
28
+ def __init__(self):
29
+ super().__init__("update-cache")
30
+
31
+ def run(self, ctx: PipelineContext) -> StepResult:
32
+ from engine.pipeline.steps.source import _resolve_modules
33
+
34
+ modules = _resolve_modules(ctx)
35
+ updated = []
36
+
37
+ for mod in modules:
38
+ name = mod["name"]
39
+ repo_dir = ctx.cache_dir / name
40
+ if not (repo_dir / ".git").exists():
41
+ continue
42
+ branch = mod.get("branch", "main")
43
+ try:
44
+ fetch_with_retry(repo_dir, branch)
45
+ updated.append(name)
46
+ ctx.state.setdefault("module_repos", {})[name] = repo_dir
47
+ ctx.state.setdefault("module_branches", {})[name] = branch
48
+ except Exception as e:
49
+ logger.warning("[update-cache] %s: %s", name, e)
50
+
51
+ return StepResult(status="ok", message=f"Updated {len(updated)} caches")
52
+
53
+
54
+ @register_step
55
+ class DetectChangesStep(Step):
56
+ """Detect changed files via git diff against stored baseline."""
57
+
58
+ default_name = "detect-changes"
59
+
60
+ def __init__(self):
61
+ super().__init__("detect-changes")
62
+
63
+ def run(self, ctx: PipelineContext) -> StepResult:
64
+ from engine.pipeline.steps.sync_finalize import _load_baseline
65
+ from core.preset import load_preset
66
+
67
+ module_repos: dict[str, Path] = ctx.state.get("module_repos", {})
68
+ if not module_repos:
69
+ return StepResult(status="skipped", message="No repos",
70
+ details={"short_circuit": True})
71
+
72
+ preset_name = ctx.kb_config.get("preset", "generic")
73
+ preset = load_preset(preset_name)
74
+ limits = preset.get("limits", {})
75
+ max_changed = limits.get("sync_max_changed_files", 20)
76
+
77
+ baseline = _load_baseline(ctx)
78
+ all_changed: list[str] = []
79
+ change_details: dict[str, list[dict]] = {}
80
+
81
+ for name, repo_path in module_repos.items():
82
+ branch = ctx.state.get("module_branches", {}).get(name, "main")
83
+ new_commit = get_head_commit(repo_path, ref=f"origin/{branch}")
84
+ if not new_commit:
85
+ continue
86
+
87
+ old_commit = baseline.get(name)
88
+ if not old_commit or old_commit == new_commit:
89
+ continue
90
+
91
+ try:
92
+ changes = diff_files(repo_path, old_commit, new_commit)
93
+ except Exception as e:
94
+ logger.error("[detect-changes] %s diff failed: %s", name, e)
95
+ continue
96
+
97
+ files = [c["file"] for c in changes]
98
+ all_changed.extend(files)
99
+ change_details[name] = changes
100
+
101
+ if not all_changed:
102
+ return StepResult(status="skipped", message="No changes detected",
103
+ details={"short_circuit": True})
104
+
105
+ if len(all_changed) > max_changed:
106
+ return StepResult(
107
+ status="failed",
108
+ message=f"Too many changes ({len(all_changed)} files) — use kb-audit",
109
+ details={"file_count": len(all_changed), "suggest_audit": True},
110
+ )
111
+
112
+ ctx.state["changed_files"] = all_changed
113
+ ctx.state["change_details"] = change_details
114
+ return StepResult(
115
+ status="ok",
116
+ message=f"Detected {len(all_changed)} changed files in {len(change_details)} modules",
117
+ )