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
core/skeleton/impact.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Change impact analysis — maps source file changes to document sections.
|
|
2
|
+
|
|
3
|
+
Determines which document sections need updating based on changed files
|
|
4
|
+
and their classifications.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from core.skeleton.impact import analyze_impact, ImpactPlan, SectionImpact
|
|
8
|
+
|
|
9
|
+
plan = analyze_impact(
|
|
10
|
+
changed_files=["src/main/.../UserService.java"],
|
|
11
|
+
module_dir=knowledge_dir / "point-base",
|
|
12
|
+
preset=preset,
|
|
13
|
+
source_cache=cache_dir / "point-base",
|
|
14
|
+
)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import re
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Literal
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Data classes
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class SectionImpact:
|
|
35
|
+
"""A single section that needs updating."""
|
|
36
|
+
|
|
37
|
+
doc_path: str # relative filename: "business-logic.md"
|
|
38
|
+
section_heading: str # "## Points Issuance Flow"
|
|
39
|
+
change_type: Literal["update", "create", "delete"]
|
|
40
|
+
affected_files: list[str] # source files that triggered this
|
|
41
|
+
confidence: float = 1.0 # mapping confidence (1.0 = exact match)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ImpactPlan:
|
|
46
|
+
"""Complete impact analysis result."""
|
|
47
|
+
|
|
48
|
+
sections: list[SectionImpact] = field(default_factory=list)
|
|
49
|
+
unmapped_files: list[str] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def total_updates(self) -> int:
|
|
53
|
+
return sum(1 for s in self.sections if s.change_type == "update")
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def total_creates(self) -> int:
|
|
57
|
+
return sum(1 for s in self.sections if s.change_type == "create")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Public API
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def analyze_impact(
|
|
66
|
+
changed_files: list[str],
|
|
67
|
+
module_dir: Path,
|
|
68
|
+
preset: dict[str, Any],
|
|
69
|
+
source_cache: Path,
|
|
70
|
+
*,
|
|
71
|
+
change_details: list[dict] | None = None,
|
|
72
|
+
) -> ImpactPlan:
|
|
73
|
+
"""Analyze which document sections are affected by source changes.
|
|
74
|
+
|
|
75
|
+
Mapping strategy:
|
|
76
|
+
1. Classify each changed file using preset file_classification rules
|
|
77
|
+
2. Determine target document(s) via 'affects' field
|
|
78
|
+
3. Extract class/method names from file path
|
|
79
|
+
4. Scan target document headings for matches
|
|
80
|
+
5. If no heading match, flag as "new content"
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
changed_files: List of changed file paths (relative to repo root)
|
|
84
|
+
module_dir: Module knowledge directory (contains .md docs)
|
|
85
|
+
preset: Preset configuration dict
|
|
86
|
+
source_cache: Source code cache directory
|
|
87
|
+
change_details: Optional git diff details [{file, status, additions, deletions}]
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
ImpactPlan with affected sections and unmapped files
|
|
91
|
+
"""
|
|
92
|
+
plan = ImpactPlan()
|
|
93
|
+
classification_rules = preset.get("file_classification", {})
|
|
94
|
+
|
|
95
|
+
# Build deletion set for special handling
|
|
96
|
+
deleted_files: set[str] = set()
|
|
97
|
+
if change_details:
|
|
98
|
+
for detail in change_details:
|
|
99
|
+
if detail.get("status") == "D":
|
|
100
|
+
deleted_files.add(detail["file"])
|
|
101
|
+
|
|
102
|
+
# Group impacts by (doc_path, section_heading) to deduplicate
|
|
103
|
+
seen: dict[tuple[str, str], SectionImpact] = {}
|
|
104
|
+
|
|
105
|
+
for filepath in changed_files:
|
|
106
|
+
# Classify file → determine target docs
|
|
107
|
+
target_docs = _classify_to_docs(filepath, classification_rules, preset)
|
|
108
|
+
|
|
109
|
+
if not target_docs:
|
|
110
|
+
plan.unmapped_files.append(filepath)
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
# Extract search terms from file path/name
|
|
114
|
+
search_terms = _extract_search_terms(filepath)
|
|
115
|
+
|
|
116
|
+
mapped = False
|
|
117
|
+
for doc_filename in target_docs:
|
|
118
|
+
doc_path = module_dir / doc_filename
|
|
119
|
+
if not doc_path.exists():
|
|
120
|
+
# Doc doesn't exist yet — can't map to sections
|
|
121
|
+
plan.unmapped_files.append(filepath)
|
|
122
|
+
mapped = True
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
# Scan document headings for matches
|
|
126
|
+
matches = _scan_doc_headings_for_match(doc_path, search_terms)
|
|
127
|
+
|
|
128
|
+
if matches:
|
|
129
|
+
for heading, confidence in matches:
|
|
130
|
+
change_type: Literal["update", "create", "delete"] = "update"
|
|
131
|
+
if filepath in deleted_files:
|
|
132
|
+
change_type = "delete"
|
|
133
|
+
|
|
134
|
+
key = (doc_filename, heading)
|
|
135
|
+
if key in seen:
|
|
136
|
+
seen[key].affected_files.append(filepath)
|
|
137
|
+
seen[key].confidence = max(seen[key].confidence, confidence)
|
|
138
|
+
else:
|
|
139
|
+
impact = SectionImpact(
|
|
140
|
+
doc_path=doc_filename,
|
|
141
|
+
section_heading=heading,
|
|
142
|
+
change_type=change_type,
|
|
143
|
+
affected_files=[filepath],
|
|
144
|
+
confidence=confidence,
|
|
145
|
+
)
|
|
146
|
+
seen[key] = impact
|
|
147
|
+
mapped = True
|
|
148
|
+
else:
|
|
149
|
+
# File classified but no section match → new content needed
|
|
150
|
+
key = (doc_filename, f"## [NEW] {search_terms[0] if search_terms else filepath}")
|
|
151
|
+
if key not in seen:
|
|
152
|
+
impact = SectionImpact(
|
|
153
|
+
doc_path=doc_filename,
|
|
154
|
+
section_heading=key[1],
|
|
155
|
+
change_type="create",
|
|
156
|
+
affected_files=[filepath],
|
|
157
|
+
confidence=0.5,
|
|
158
|
+
)
|
|
159
|
+
seen[key] = impact
|
|
160
|
+
else:
|
|
161
|
+
seen[key].affected_files.append(filepath)
|
|
162
|
+
mapped = True
|
|
163
|
+
|
|
164
|
+
if not mapped:
|
|
165
|
+
plan.unmapped_files.append(filepath)
|
|
166
|
+
|
|
167
|
+
plan.sections = list(seen.values())
|
|
168
|
+
# Sort by confidence descending, then by doc_path
|
|
169
|
+
plan.sections.sort(key=lambda s: (-s.confidence, s.doc_path, s.section_heading))
|
|
170
|
+
|
|
171
|
+
logger.info(
|
|
172
|
+
"[impact] %d changed files → %d section impacts, %d unmapped",
|
|
173
|
+
len(changed_files), len(plan.sections), len(plan.unmapped_files),
|
|
174
|
+
)
|
|
175
|
+
return plan
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Internal helpers
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _classify_to_docs(
|
|
184
|
+
filepath: str,
|
|
185
|
+
classification_rules: dict[str, Any],
|
|
186
|
+
preset: dict[str, Any],
|
|
187
|
+
) -> list[str]:
|
|
188
|
+
"""Classify a file and return target document filenames.
|
|
189
|
+
|
|
190
|
+
Uses the 'affects' field from file_classification rules.
|
|
191
|
+
Falls back to pattern matching if no skeleton entry available.
|
|
192
|
+
"""
|
|
193
|
+
from core.preset_classify import classify_file
|
|
194
|
+
|
|
195
|
+
# Classify using full rules (without skeleton entry, pattern-only)
|
|
196
|
+
categories = classify_file(preset, filepath, skeleton_entry=None)
|
|
197
|
+
|
|
198
|
+
# Map categories → affects → doc filenames
|
|
199
|
+
doc_filenames: list[str] = []
|
|
200
|
+
for cat in categories:
|
|
201
|
+
cfg = classification_rules.get(cat, {})
|
|
202
|
+
affects = cfg.get("affects", [])
|
|
203
|
+
for doc_file in affects:
|
|
204
|
+
if doc_file not in doc_filenames:
|
|
205
|
+
doc_filenames.append(doc_file)
|
|
206
|
+
|
|
207
|
+
return doc_filenames
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _extract_search_terms(filepath: str) -> list[str]:
|
|
211
|
+
"""Extract search terms from a file path for heading matching.
|
|
212
|
+
|
|
213
|
+
Extracts:
|
|
214
|
+
- Class name (from filename without extension)
|
|
215
|
+
- Package-derived terms
|
|
216
|
+
- Endpoint URLs (from common patterns)
|
|
217
|
+
"""
|
|
218
|
+
terms: list[str] = []
|
|
219
|
+
|
|
220
|
+
# Extract filename without extension
|
|
221
|
+
basename = filepath.rsplit("/", 1)[-1] if "/" in filepath else filepath
|
|
222
|
+
basename = basename.rsplit("\\", 1)[-1] if "\\" in basename else basename
|
|
223
|
+
name_no_ext = basename.rsplit(".", 1)[0] if "." in basename else basename
|
|
224
|
+
|
|
225
|
+
if name_no_ext:
|
|
226
|
+
terms.append(name_no_ext)
|
|
227
|
+
|
|
228
|
+
# Split CamelCase into words for fuzzy matching
|
|
229
|
+
words = re.sub(r"([a-z])([A-Z])", r"\1 \2", name_no_ext).split()
|
|
230
|
+
# Remove common suffixes for better matching
|
|
231
|
+
clean_words = [w for w in words if w.lower() not in (
|
|
232
|
+
"service", "impl", "controller", "mapper", "dao",
|
|
233
|
+
"repository", "config", "configuration", "handler",
|
|
234
|
+
)]
|
|
235
|
+
if clean_words and clean_words != [name_no_ext]:
|
|
236
|
+
terms.append("".join(clean_words))
|
|
237
|
+
|
|
238
|
+
return terms
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _scan_doc_headings_for_match(
|
|
242
|
+
doc_path: Path,
|
|
243
|
+
search_terms: list[str],
|
|
244
|
+
) -> list[tuple[str, float]]:
|
|
245
|
+
"""Scan document headings for terms matching changed entities.
|
|
246
|
+
|
|
247
|
+
Returns: [(heading_line, confidence_score)]
|
|
248
|
+
"""
|
|
249
|
+
try:
|
|
250
|
+
content = doc_path.read_text(encoding="utf-8", errors="replace")
|
|
251
|
+
except OSError:
|
|
252
|
+
return []
|
|
253
|
+
|
|
254
|
+
headings: list[str] = []
|
|
255
|
+
for line in content.splitlines():
|
|
256
|
+
if line.startswith("## ") or line.startswith("### "):
|
|
257
|
+
headings.append(line)
|
|
258
|
+
|
|
259
|
+
matches: list[tuple[str, float]] = []
|
|
260
|
+
|
|
261
|
+
for term in search_terms:
|
|
262
|
+
term_lower = term.lower()
|
|
263
|
+
for heading in headings:
|
|
264
|
+
heading_lower = heading.lower()
|
|
265
|
+
# Exact class name match in heading
|
|
266
|
+
if term_lower in heading_lower:
|
|
267
|
+
# Higher confidence for exact match
|
|
268
|
+
confidence = 0.95 if term == search_terms[0] else 0.8
|
|
269
|
+
if (heading, confidence) not in matches:
|
|
270
|
+
matches.append((heading, confidence))
|
|
271
|
+
|
|
272
|
+
# Deduplicate, keep highest confidence per heading
|
|
273
|
+
best: dict[str, float] = {}
|
|
274
|
+
for heading, conf in matches:
|
|
275
|
+
if heading not in best or conf > best[heading]:
|
|
276
|
+
best[heading] = conf
|
|
277
|
+
|
|
278
|
+
return [(h, c) for h, c in best.items()]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""JAR download — HTTP download of Maven source JARs with caching.
|
|
2
|
+
|
|
3
|
+
Downloads sources.jar files from Maven repositories (Nexus/Artifactory)
|
|
4
|
+
with retry logic, authentication support, and local caching.
|
|
5
|
+
|
|
6
|
+
On failure: logs warning and returns None (never aborts the pipeline).
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from core.skeleton.jar_download import download_source_jar, is_cached
|
|
10
|
+
|
|
11
|
+
if not is_cached(group_id, artifact_id, version, cache_dir):
|
|
12
|
+
path = download_source_jar(group_id, artifact_id, version, repo_url, cache_dir)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import logging
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from urllib.error import HTTPError, URLError
|
|
21
|
+
from urllib.request import Request, urlopen
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Constants
|
|
26
|
+
DOWNLOAD_TIMEOUT = 60 # seconds
|
|
27
|
+
MAX_RETRIES = 2
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Public API
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def download_source_jar(
|
|
36
|
+
group_id: str,
|
|
37
|
+
artifact_id: str,
|
|
38
|
+
version: str,
|
|
39
|
+
repo_url: str,
|
|
40
|
+
cache_dir: Path,
|
|
41
|
+
auth: str | None = None,
|
|
42
|
+
) -> Path | None:
|
|
43
|
+
"""Download a sources.jar from Maven repository.
|
|
44
|
+
|
|
45
|
+
Tries sources.jar first, then falls back to regular .jar.
|
|
46
|
+
Results are cached locally to avoid repeated downloads.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
group_id: Maven groupId (e.g. "com.example")
|
|
50
|
+
artifact_id: Maven artifactId (e.g. "my-lib")
|
|
51
|
+
version: Maven version (e.g. "1.2.3")
|
|
52
|
+
repo_url: Repository base URL
|
|
53
|
+
cache_dir: Local cache directory
|
|
54
|
+
auth: Optional "user:password" for Basic auth
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Path to downloaded JAR file, or None on failure.
|
|
58
|
+
"""
|
|
59
|
+
# Check cache first
|
|
60
|
+
cached = _get_cache_path(group_id, artifact_id, version, cache_dir, "sources")
|
|
61
|
+
if cached.exists() and cached.stat().st_size > 0:
|
|
62
|
+
logger.debug("Cache hit: %s", cached)
|
|
63
|
+
return cached
|
|
64
|
+
|
|
65
|
+
# Try sources.jar
|
|
66
|
+
sources_url = _build_maven_url(repo_url, group_id, artifact_id, version, "sources")
|
|
67
|
+
if _download_file(sources_url, cached, auth):
|
|
68
|
+
logger.info("Downloaded sources.jar: %s:%s:%s", group_id, artifact_id, version)
|
|
69
|
+
return cached
|
|
70
|
+
|
|
71
|
+
# Try regular .jar as fallback
|
|
72
|
+
regular_cached = _get_cache_path(group_id, artifact_id, version, cache_dir, "")
|
|
73
|
+
regular_url = _build_maven_url(repo_url, group_id, artifact_id, version, "")
|
|
74
|
+
if _download_file(regular_url, regular_cached, auth):
|
|
75
|
+
logger.info("Downloaded regular jar: %s:%s:%s", group_id, artifact_id, version)
|
|
76
|
+
return regular_cached
|
|
77
|
+
|
|
78
|
+
logger.warning("Failed to download: %s:%s:%s", group_id, artifact_id, version)
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def is_cached(
|
|
83
|
+
group_id: str,
|
|
84
|
+
artifact_id: str,
|
|
85
|
+
version: str,
|
|
86
|
+
cache_dir: Path,
|
|
87
|
+
) -> bool:
|
|
88
|
+
"""Check if a JAR is already cached locally.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
group_id: Maven groupId
|
|
92
|
+
artifact_id: Maven artifactId
|
|
93
|
+
version: Maven version
|
|
94
|
+
cache_dir: Local cache directory
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
True if sources.jar or regular .jar exists in cache.
|
|
98
|
+
"""
|
|
99
|
+
sources = _get_cache_path(group_id, artifact_id, version, cache_dir, "sources")
|
|
100
|
+
if sources.exists() and sources.stat().st_size > 0:
|
|
101
|
+
return True
|
|
102
|
+
regular = _get_cache_path(group_id, artifact_id, version, cache_dir, "")
|
|
103
|
+
return regular.exists() and regular.stat().st_size > 0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# Internal
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _get_cache_path(
|
|
112
|
+
group_id: str, artifact_id: str, version: str,
|
|
113
|
+
cache_dir: Path, classifier: str,
|
|
114
|
+
) -> Path:
|
|
115
|
+
"""Compute local cache path for a JAR."""
|
|
116
|
+
group_path = group_id.replace(".", "/")
|
|
117
|
+
filename = f"{artifact_id}-{version}"
|
|
118
|
+
if classifier:
|
|
119
|
+
filename += f"-{classifier}"
|
|
120
|
+
filename += ".jar"
|
|
121
|
+
return cache_dir / group_path / artifact_id / version / filename
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _build_maven_url(
|
|
125
|
+
repo_url: str, group_id: str, artifact_id: str,
|
|
126
|
+
version: str, classifier: str,
|
|
127
|
+
) -> str:
|
|
128
|
+
"""Build Maven repository download URL."""
|
|
129
|
+
repo_url = repo_url.rstrip("/")
|
|
130
|
+
group_path = group_id.replace(".", "/")
|
|
131
|
+
filename = f"{artifact_id}-{version}"
|
|
132
|
+
if classifier:
|
|
133
|
+
filename += f"-{classifier}"
|
|
134
|
+
filename += ".jar"
|
|
135
|
+
return f"{repo_url}/{group_path}/{artifact_id}/{version}/{filename}"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _download_file(url: str, dest: Path, auth: str | None = None) -> bool:
|
|
139
|
+
"""HTTP download with retry. Returns True on success."""
|
|
140
|
+
for attempt in range(MAX_RETRIES + 1):
|
|
141
|
+
try:
|
|
142
|
+
req = Request(url)
|
|
143
|
+
if auth:
|
|
144
|
+
credentials = base64.b64encode(auth.encode()).decode()
|
|
145
|
+
req.add_header("Authorization", f"Basic {credentials}")
|
|
146
|
+
|
|
147
|
+
with urlopen(req, timeout=DOWNLOAD_TIMEOUT) as resp:
|
|
148
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
with open(dest, "wb") as f:
|
|
150
|
+
while True:
|
|
151
|
+
chunk = resp.read(8192)
|
|
152
|
+
if not chunk:
|
|
153
|
+
break
|
|
154
|
+
f.write(chunk)
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
except HTTPError as e:
|
|
158
|
+
if e.code == 404:
|
|
159
|
+
return False
|
|
160
|
+
if attempt < MAX_RETRIES:
|
|
161
|
+
logger.debug("HTTP %d on attempt %d: %s", e.code, attempt + 1, url)
|
|
162
|
+
continue
|
|
163
|
+
logger.warning("HTTP error %d: %s", e.code, url)
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
except URLError as e:
|
|
167
|
+
if attempt < MAX_RETRIES:
|
|
168
|
+
logger.debug("Network error on attempt %d: %s", attempt + 1, e.reason)
|
|
169
|
+
continue
|
|
170
|
+
logger.warning("Network error: %s — %s", e.reason, url)
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
except OSError as e:
|
|
174
|
+
logger.warning("IO error downloading %s: %s", url, e)
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
return False
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""JAR resolver — orchestrate POM parsing, JAR download, and extraction.
|
|
2
|
+
|
|
3
|
+
Coordinates the full workflow: parse POM → identify internal dependencies →
|
|
4
|
+
download source JARs → extract .java files → return paths.
|
|
5
|
+
|
|
6
|
+
Uses pom_parser.py for coordinate extraction and jar_download.py for downloading.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from core.skeleton.jar_resolver import resolve_jar_sources, extract_jar
|
|
10
|
+
|
|
11
|
+
paths = resolve_jar_sources(pom_path, cache_dir, repo_url, auth)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import zipfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Public API
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_jar_sources(
|
|
29
|
+
pom_path: Path,
|
|
30
|
+
cache_dir: Path,
|
|
31
|
+
repo_url: str,
|
|
32
|
+
auth: str | None = None,
|
|
33
|
+
internal_group_ids: list[str] | None = None,
|
|
34
|
+
known_modules: set[str] | None = None,
|
|
35
|
+
exclude_artifacts: set[str] | None = None,
|
|
36
|
+
) -> list[Path]:
|
|
37
|
+
"""Orchestrate: parse POM → download JARs → extract → return paths.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
pom_path: Path to pom.xml
|
|
41
|
+
cache_dir: Directory for caching downloaded JARs and extracted sources
|
|
42
|
+
repo_url: Maven repository URL
|
|
43
|
+
auth: Optional "user:password" for Basic auth
|
|
44
|
+
internal_group_ids: Group ID prefixes to consider internal (auto-inferred if None)
|
|
45
|
+
known_modules: Module names that already have source (skip these)
|
|
46
|
+
exclude_artifacts: Artifact IDs to exclude
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
List of directories containing extracted .java source files.
|
|
50
|
+
"""
|
|
51
|
+
from core.skeleton.pom_parser import parse_pom, extract_dependencies
|
|
52
|
+
from core.skeleton.jar_download import download_source_jar, is_cached
|
|
53
|
+
|
|
54
|
+
if not pom_path.exists():
|
|
55
|
+
logger.warning("POM file not found: %s", pom_path)
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
known_modules = known_modules or set()
|
|
59
|
+
exclude_artifacts = exclude_artifacts or set()
|
|
60
|
+
|
|
61
|
+
# Parse POM and get dependencies
|
|
62
|
+
pom = parse_pom(pom_path)
|
|
63
|
+
all_deps = extract_dependencies(pom_path)
|
|
64
|
+
|
|
65
|
+
# Auto-infer internal group IDs from POM's own groupId
|
|
66
|
+
if not internal_group_ids:
|
|
67
|
+
own_gid = pom.get("group_id", "")
|
|
68
|
+
if own_gid:
|
|
69
|
+
internal_group_ids = [own_gid]
|
|
70
|
+
else:
|
|
71
|
+
logger.warning("Cannot infer internal groupId from POM")
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
# Filter to internal dependencies
|
|
75
|
+
internal_deps = _filter_internal_deps(
|
|
76
|
+
all_deps, internal_group_ids, known_modules, exclude_artifacts
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
if not internal_deps:
|
|
80
|
+
logger.info("No internal dependencies to resolve")
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
# Download and extract each dependency
|
|
84
|
+
result_paths: list[Path] = []
|
|
85
|
+
for dep in internal_deps:
|
|
86
|
+
gid = dep["groupId"]
|
|
87
|
+
aid = dep["artifactId"]
|
|
88
|
+
version = dep.get("version", "")
|
|
89
|
+
|
|
90
|
+
if not version:
|
|
91
|
+
logger.warning("No version for %s:%s, skipping", gid, aid)
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
output_dir = cache_dir / f"{aid}-sources"
|
|
95
|
+
|
|
96
|
+
# Skip if already extracted
|
|
97
|
+
if output_dir.exists() and any(output_dir.rglob("*.java")):
|
|
98
|
+
logger.debug("Already extracted: %s", aid)
|
|
99
|
+
result_paths.append(output_dir)
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
# Download JAR
|
|
103
|
+
jar_path = download_source_jar(gid, aid, version, repo_url, cache_dir, auth)
|
|
104
|
+
if not jar_path:
|
|
105
|
+
logger.warning("Failed to download %s:%s:%s", gid, aid, version)
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
# Extract
|
|
109
|
+
count = extract_jar(jar_path, output_dir)
|
|
110
|
+
if count > 0:
|
|
111
|
+
logger.info("Extracted %d .java files from %s", count, aid)
|
|
112
|
+
result_paths.append(output_dir)
|
|
113
|
+
else:
|
|
114
|
+
logger.warning("No .java files in %s:%s:%s", gid, aid, version)
|
|
115
|
+
|
|
116
|
+
return result_paths
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def extract_jar(jar_path: Path, dest_dir: Path) -> int:
|
|
120
|
+
"""Extract .java files from a JAR (sources.jar or regular .jar).
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
jar_path: Path to the JAR file
|
|
124
|
+
dest_dir: Destination directory for extracted files
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Number of .java files extracted.
|
|
128
|
+
"""
|
|
129
|
+
if not jar_path.exists():
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
count = 0
|
|
134
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
|
|
136
|
+
with zipfile.ZipFile(jar_path, "r") as zf:
|
|
137
|
+
for info in zf.infolist():
|
|
138
|
+
if info.filename.endswith(".java") and not info.is_dir():
|
|
139
|
+
zf.extract(info, dest_dir)
|
|
140
|
+
count += 1
|
|
141
|
+
|
|
142
|
+
return count
|
|
143
|
+
|
|
144
|
+
except (zipfile.BadZipFile, OSError) as e:
|
|
145
|
+
logger.warning("Failed to extract %s: %s", jar_path, e)
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# Internal
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _filter_internal_deps(
|
|
155
|
+
all_deps: list[dict],
|
|
156
|
+
internal_group_ids: list[str],
|
|
157
|
+
known_modules: set[str],
|
|
158
|
+
exclude_artifacts: set[str],
|
|
159
|
+
) -> list[dict]:
|
|
160
|
+
"""Filter dependencies to only internal ones that need resolution."""
|
|
161
|
+
result: list[dict] = []
|
|
162
|
+
|
|
163
|
+
for dep in all_deps:
|
|
164
|
+
gid = dep.get("groupId", "")
|
|
165
|
+
aid = dep.get("artifactId", "")
|
|
166
|
+
scope = dep.get("scope", "compile")
|
|
167
|
+
|
|
168
|
+
# Skip test/provided scope
|
|
169
|
+
if scope in ("test", "provided"):
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
# Check groupId matches internal prefix
|
|
173
|
+
if not any(gid.startswith(prefix) for prefix in internal_group_ids):
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
# Skip already-known modules
|
|
177
|
+
if aid in known_modules:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
# Skip excluded artifacts
|
|
181
|
+
if aid in exclude_artifacts:
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
result.append(dep)
|
|
185
|
+
|
|
186
|
+
return result
|