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.
- cli/__init__.py +50 -0
- cli/__main__.py +5 -0
- cli/commands/__init__.py +1 -0
- cli/commands/anchor_fix.py +47 -0
- cli/commands/diff_doc.py +52 -0
- cli/commands/dispatch.py +77 -0
- cli/commands/extract.py +72 -0
- cli/commands/file_list.py +74 -0
- cli/commands/index.py +84 -0
- cli/commands/lock.py +89 -0
- cli/commands/merge.py +60 -0
- cli/commands/merge_delta.py +19 -0
- cli/commands/metadata.py +24 -0
- cli/commands/pipeline.py +45 -0
- cli/commands/post_merge.py +43 -0
- cli/commands/query.py +52 -0
- cli/commands/render.py +101 -0
- cli/commands/scan_repos.py +46 -0
- cli/commands/setup.py +94 -0
- cli/commands/split.py +196 -0
- cli/commands/stale_files.py +98 -0
- cli/commands/validate.py +191 -0
- core/__init__.py +32 -0
- core/config.py +261 -0
- core/docs/__init__.py +7 -0
- core/docs/section_updater.py +286 -0
- core/docs/shared.py +149 -0
- core/git.py +294 -0
- core/interfaces.py +249 -0
- core/monitor/__init__.py +5 -0
- core/monitor/progress.py +83 -0
- core/monitor/prompt_store.py +49 -0
- core/paths.py +141 -0
- core/preset.py +237 -0
- core/preset_accessors.py +202 -0
- core/preset_classify.py +132 -0
- core/preset_hooks.py +129 -0
- core/preset_profile.py +89 -0
- core/prompt/__init__.py +7 -0
- core/prompt/__main__.py +147 -0
- core/prompt/content.py +320 -0
- core/prompt/context_manager.py +164 -0
- core/prompt/renderer.py +236 -0
- core/prompt/response_parser.py +274 -0
- core/prompt/templates.py +357 -0
- core/prompt/validate_parity.py +162 -0
- core/prompt/variables.py +339 -0
- core/rag/__init__.py +22 -0
- core/rag/__main__.py +136 -0
- core/rag/bm25_index.py +268 -0
- core/rag/chunker.py +273 -0
- core/rag/embedder.py +151 -0
- core/rag/indexer.py +292 -0
- core/rag/loader.py +89 -0
- core/rag/retriever.py +82 -0
- core/skeleton/__init__.py +11 -0
- core/skeleton/__main__.py +934 -0
- core/skeleton/anchor_fix.py +250 -0
- core/skeleton/classify.py +331 -0
- core/skeleton/cmd_anchor_fix.py +43 -0
- core/skeleton/cmd_diff_doc.py +44 -0
- core/skeleton/cmd_lock.py +87 -0
- core/skeleton/cmd_merge_delta.py +41 -0
- core/skeleton/community.py +233 -0
- core/skeleton/dependency_graph.py +306 -0
- core/skeleton/diff_doc.py +248 -0
- core/skeleton/dispatch.py +273 -0
- core/skeleton/dispatch_render.py +319 -0
- core/skeleton/dispatch_source.py +111 -0
- core/skeleton/extract.py +218 -0
- core/skeleton/extract_methods.py +298 -0
- core/skeleton/file_list.py +239 -0
- core/skeleton/impact.py +278 -0
- core/skeleton/jar_download.py +177 -0
- core/skeleton/jar_resolver.py +186 -0
- core/skeleton/loader.py +162 -0
- core/skeleton/merge.py +278 -0
- core/skeleton/merge_delta.py +229 -0
- core/skeleton/metadata.py +96 -0
- core/skeleton/metadata_builders.py +264 -0
- core/skeleton/module_dag.py +330 -0
- core/skeleton/parsers/__init__.py +71 -0
- core/skeleton/parsers/jqassistant.py +300 -0
- core/skeleton/parsers/jqassistant_cypher.py +225 -0
- core/skeleton/parsers/regex.py +171 -0
- core/skeleton/parsers/treesitter.py +324 -0
- core/skeleton/parsers/treesitter_java.py +284 -0
- core/skeleton/parsers/treesitter_multi.py +289 -0
- core/skeleton/pom_parser.py +299 -0
- core/skeleton/post_merge.py +295 -0
- core/skeleton/post_merge_llm.py +82 -0
- core/skeleton/query.py +195 -0
- core/skeleton/shard_context.py +177 -0
- core/skeleton/split.py +180 -0
- core/skeleton/split_cache.py +107 -0
- core/skeleton/split_feedback.py +174 -0
- core/skeleton/split_plan.py +219 -0
- core/skeleton/split_plan_helpers.py +305 -0
- core/skeleton/split_plan_llm.py +274 -0
- core/utils.py +135 -0
- core/validators/__init__.py +65 -0
- core/validators/__main__.py +215 -0
- core/validators/consistency.py +203 -0
- core/validators/coverage.py +171 -0
- core/validators/duplicates.py +76 -0
- core/validators/engine.py +224 -0
- core/validators/links.py +76 -0
- core/validators/sampling.py +169 -0
- core/validators/structure.py +144 -0
- engine/__init__.py +7 -0
- engine/assembler.py +231 -0
- engine/confirm.py +65 -0
- engine/dedup.py +106 -0
- engine/main.py +211 -0
- engine/pipeline/__init__.py +163 -0
- engine/pipeline/recovery.py +250 -0
- engine/pipeline/steps/__init__.py +23 -0
- engine/pipeline/steps/audit.py +220 -0
- engine/pipeline/steps/audit_apply.py +195 -0
- engine/pipeline/steps/audit_helpers.py +155 -0
- engine/pipeline/steps/classify_llm.py +236 -0
- engine/pipeline/steps/classify_prompt.py +223 -0
- engine/pipeline/steps/finalize.py +160 -0
- engine/pipeline/steps/generate.py +169 -0
- engine/pipeline/steps/generate_batch.py +197 -0
- engine/pipeline/steps/generate_recovery.py +170 -0
- engine/pipeline/steps/llm_plan_split.py +253 -0
- engine/pipeline/steps/lock.py +64 -0
- engine/pipeline/steps/preflight.py +237 -0
- engine/pipeline/steps/preflight_adjust.py +147 -0
- engine/pipeline/steps/pregenerate.py +130 -0
- engine/pipeline/steps/quality.py +81 -0
- engine/pipeline/steps/skeleton.py +149 -0
- engine/pipeline/steps/source.py +163 -0
- engine/pipeline/steps/sync.py +117 -0
- engine/pipeline/steps/sync_finalize.py +237 -0
- engine/pipeline/steps/sync_update.py +341 -0
- engine/pipelines.py +91 -0
- engine/runner.py +335 -0
- engine/strategies/__init__.py +86 -0
- engine/strategies/api.py +128 -0
- engine/strategies/delegated.py +50 -0
- engine/strategies/dryrun.py +25 -0
- engine/two_phase.py +143 -0
- mcp_server/__init__.py +73 -0
- mcp_server/__main__.py +5 -0
- mcp_server/tools/__init__.py +1 -0
- mcp_server/tools/config.py +63 -0
- mcp_server/tools/discovery.py +276 -0
- mcp_server/tools/generation.py +184 -0
- mcp_server/tools/planning.py +144 -0
- mcp_server/tools/source.py +175 -0
- mcp_server/tools/validation.py +140 -0
- mcp_server/tools/workflow.py +166 -0
- mcp_server/workflow_loader.py +204 -0
- presets/generic/audit_dimensions.md +132 -0
- presets/generic/doc_types.yaml +152 -0
- presets/generic/preset.yaml +115 -0
- presets/java-spring/audit_dimensions.md +228 -0
- presets/java-spring/audit_dimensions.yaml +203 -0
- presets/java-spring/doc_types.yaml +269 -0
- presets/java-spring/hooks.py +122 -0
- presets/java-spring/preset.yaml +341 -0
- presets/java-spring/templates/README.md +34 -0
- presets/java-spring/templates/audit-system.md +15 -0
- presets/java-spring/templates/subagent-aop.md +105 -0
- presets/java-spring/templates/subagent-api.md +63 -0
- presets/java-spring/templates/subagent-architecture.md +111 -0
- presets/java-spring/templates/subagent-async-events.md +107 -0
- presets/java-spring/templates/subagent-audit-api-contracts.md +40 -0
- presets/java-spring/templates/subagent-audit-architecture.md +38 -0
- presets/java-spring/templates/subagent-audit-business.md +40 -0
- presets/java-spring/templates/subagent-audit-data-models.md +40 -0
- presets/java-spring/templates/subagent-business.md +129 -0
- presets/java-spring/templates/subagent-caching.md +75 -0
- presets/java-spring/templates/subagent-database-access.md +114 -0
- presets/java-spring/templates/subagent-enum.md +75 -0
- presets/java-spring/templates/subagent-error-handling.md +91 -0
- presets/java-spring/templates/subagent-external-integrations.md +80 -0
- presets/java-spring/templates/subagent-index.md +122 -0
- presets/java-spring/templates/subagent-messaging.md +97 -0
- presets/java-spring/templates/subagent-model.md +88 -0
- presets/java-spring/templates/subagent-observability.md +91 -0
- presets/java-spring/templates/subagent-scheduled.md +81 -0
- presets/java-spring/templates/subagent-security.md +102 -0
- presets/java-spring/templates/subagent-structure.md +101 -0
- presets/java-spring/templates/subagent-sync-section.md +34 -0
- presets/java-spring/templates/subagent-utils.md +73 -0
- presets/java-spring/templates/sync-system.md +8 -0
- presets/java-spring/workflow-extensions.md +112 -0
- skills/__init__.py +1 -0
- skills/_shared/README.md +30 -0
- skills/_shared/doc-coverage-shared.md +134 -0
- skills/_shared/doc-quality-standard.md +1058 -0
- skills/_shared/doc-subagent-rules.md +762 -0
- skills/_shared/windows-compat.md +89 -0
- skills/kb-audit/SKILL.md +52 -0
- skills/kb-audit/rules.md +88 -0
- skills/kb-audit/steps/step-01-prepare.md +75 -0
- skills/kb-audit/steps/step-02-audit.md +96 -0
- skills/kb-audit/steps/step-03-verify.md +65 -0
- skills/kb-audit/steps/step-04-report.md +64 -0
- skills/kb-init/SKILL.md +146 -0
- skills/kb-init/rules.md +187 -0
- skills/kb-init/steps/step-01-scope.md +62 -0
- skills/kb-init/steps/step-02-source.md +410 -0
- skills/kb-init/steps/step-03-generate.md +307 -0
- skills/kb-init/steps/step-04-quality.md +92 -0
- skills/kb-init/steps/step-05-finalize.md +132 -0
- skills/kb-init/templates/core/execution-modes.md +29 -0
- skills/kb-init/templates/core/output-only.md +4 -0
- skills/kb-init/templates/core/readwrite.md +33 -0
- skills/kb-search/SKILL.md +138 -0
- skills/kb-search/rules.md +64 -0
- skills/kb-sync/SKILL.md +43 -0
- skills/kb-sync/rules.md +70 -0
- skills/kb-sync/scripts/rebuild_module.py +91 -0
- skills/kb-sync/scripts/scan_repos.py +687 -0
- skills/kb-sync/steps/step-01-detect.md +72 -0
- skills/kb-sync/steps/step-02-update.md +71 -0
- skills/kb-sync/steps/step-03-verify.md +47 -0
- skills/kb-sync/steps/step-04-finalize.md +52 -0
- source_kb-0.2.2.dist-info/METADATA +194 -0
- source_kb-0.2.2.dist-info/RECORD +228 -0
- source_kb-0.2.2.dist-info/WHEEL +5 -0
- source_kb-0.2.2.dist-info/entry_points.txt +3 -0
- source_kb-0.2.2.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|