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,171 @@
1
+ """Regex-based skeleton parser — zero-dependency fallback.
2
+
3
+ Uses language-specific regex patterns (loaded from preset config) to extract
4
+ class/method declarations. Less accurate than tree-sitter but works everywhere.
5
+
6
+ Pattern configuration comes from preset.yaml → parser_config.regex_rules.
7
+ If no rules configured, uses built-in defaults for common languages.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import re
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from core.interfaces import SkeletonParser
18
+ from core.skeleton.parsers import register_parser
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Built-in regex patterns per language (used when preset doesn't define custom rules)
23
+ DEFAULT_PATTERNS: dict[str, dict[str, str]] = {
24
+ "java": {
25
+ "class": r"^\s*(?:(?:public|protected|private|abstract|final|static)\s+)*(?:class|interface|enum|record)\s+(\w+)",
26
+ "method": r"^\s*(?:(?:public|protected|private|static|final|abstract|synchronized|native)\s+)*[\w<>\[\],\s]+\s+(\w+)\s*\(",
27
+ "field": r"^\s*(?:(?:public|protected|private|static|final|volatile|transient)\s+)*[\w<>\[\],\s]+\s+(\w+)\s*[;=]",
28
+ },
29
+ "python": {
30
+ "class": r"^\s*class\s+(\w+)",
31
+ "method": r"^\s*(?:async\s+)?def\s+(\w+)\s*\(",
32
+ },
33
+ "go": {
34
+ "class": r"^type\s+(\w+)\s+struct\b",
35
+ "method": r"^func\s+(?:\(\w+\s+\*?\w+\)\s+)?(\w+)\s*\(",
36
+ },
37
+ "typescript": {
38
+ "class": r"^\s*(?:export\s+)?(?:abstract\s+)?(?:class|interface|enum)\s+(\w+)",
39
+ "method": r"^\s*(?:(?:public|private|protected|static|async|abstract)\s+)*(\w+)\s*\(",
40
+ },
41
+ "kotlin": {
42
+ "class": r"^\s*(?:(?:open|abstract|data|sealed|internal|private)\s+)*(?:class|interface|object|enum\s+class)\s+(\w+)",
43
+ "method": r"^\s*(?:(?:override|open|private|internal|suspend)\s+)*fun\s+(\w+)\s*\(",
44
+ },
45
+ }
46
+
47
+ # Extension → language key
48
+ EXT_TO_LANG: dict[str, str] = {
49
+ ".java": "java", ".py": "python", ".go": "go",
50
+ ".ts": "typescript", ".tsx": "typescript", ".js": "typescript",
51
+ ".kt": "kotlin", ".kts": "kotlin",
52
+ ".cs": "java", # C# patterns similar enough to Java for basic extraction
53
+ ".scala": "java",
54
+ }
55
+
56
+ BRANCH_PATTERN = re.compile(
57
+ r"^\s*(?:if|else|elif|switch|case|match|for|while|catch|except|when)\b"
58
+ )
59
+
60
+
61
+ @register_parser
62
+ class RegexSkeletonParser(SkeletonParser):
63
+ """Regex-based fallback parser. Registered as 'regex'. Lowest priority."""
64
+
65
+ name = "regex"
66
+ priority = 50
67
+
68
+ def can_parse(self, repo_path: Path, preset: dict[str, Any]) -> bool:
69
+ """Always available — zero dependencies."""
70
+ return True
71
+
72
+ def parse(self, repo_path: Path, preset: dict[str, Any], **kwargs: Any) -> list[dict[str, Any]]:
73
+ """Parse source files using regex patterns from preset config."""
74
+ from core.git import ls_tree
75
+
76
+ ref = kwargs.get("ref", "HEAD")
77
+ subpath = kwargs.get("subpath")
78
+ parser_config = preset.get("parser_config", {})
79
+ extensions = parser_config.get("extensions") or list(EXT_TO_LANG.keys())
80
+ skip_dirs = set(parser_config.get("skip_dirs", ["test", "tests", "generated"]))
81
+ custom_rules = parser_config.get("regex_rules", {})
82
+
83
+ all_files = ls_tree(repo_path, ref)
84
+
85
+ subpath_prefix = ""
86
+ if subpath:
87
+ subpath_prefix = subpath.rstrip("/") + "/"
88
+ all_files = [f for f in all_files if f.startswith(subpath_prefix)]
89
+
90
+ entries: list[dict[str, Any]] = []
91
+
92
+ for filepath in all_files:
93
+ rel_path = filepath[len(subpath_prefix):] if subpath_prefix else filepath
94
+ if any(f"/{d}/" in rel_path or rel_path.startswith(f"{d}/") for d in skip_dirs):
95
+ continue
96
+ if not any(rel_path.endswith(ext) for ext in extensions):
97
+ continue
98
+
99
+ content = _read_file(repo_path, ref, filepath)
100
+ if content is None:
101
+ continue
102
+
103
+ entry = _parse_file(content, rel_path, custom_rules)
104
+ if entry:
105
+ entries.append(entry)
106
+
107
+ return entries
108
+
109
+
110
+ def _read_file(repo_path: Path, ref: str, filepath: str) -> str | None:
111
+ """Read file content from git or filesystem."""
112
+ from core.git import read_file
113
+ try:
114
+ return read_file(repo_path, ref, filepath)
115
+ except Exception:
116
+ full = repo_path / filepath
117
+ if full.exists():
118
+ try:
119
+ return full.read_text(encoding="utf-8")
120
+ except (OSError, UnicodeDecodeError):
121
+ pass
122
+ return None
123
+
124
+
125
+ def _parse_file(content: str, filepath: str, custom_rules: dict) -> dict[str, Any] | None:
126
+ """Parse a single file using regex patterns."""
127
+ ext = Path(filepath).suffix.lower()
128
+ lang = EXT_TO_LANG.get(ext)
129
+ if not lang:
130
+ return None
131
+
132
+ # Use custom rules if provided, otherwise built-in defaults
133
+ patterns = custom_rules if custom_rules else DEFAULT_PATTERNS.get(lang, {})
134
+ if not patterns:
135
+ return None
136
+
137
+ lines = content.splitlines()
138
+ total_lines = len(lines)
139
+
140
+ class_re = re.compile(patterns["class"]) if "class" in patterns else None
141
+ method_re = re.compile(patterns["method"]) if "method" in patterns else None
142
+
143
+ classes: list[dict] = []
144
+ method_starts: list[tuple[str, int]] = []
145
+
146
+ for i, line in enumerate(lines):
147
+ if class_re:
148
+ m = class_re.match(line)
149
+ if m:
150
+ classes.append({"name": m.group(1)})
151
+ if method_re:
152
+ m = method_re.match(line)
153
+ if m:
154
+ method_starts.append((m.group(1), i))
155
+
156
+ # Compute method metrics
157
+ methods: list[dict] = []
158
+ for idx, (name, start) in enumerate(method_starts):
159
+ end = method_starts[idx + 1][1] if idx + 1 < len(method_starts) else total_lines
160
+ line_count = end - start
161
+ branches = sum(1 for l in lines[start:end] if BRANCH_PATTERN.match(l))
162
+ complexity = "high" if line_count > 50 or branches > 10 else "medium" if line_count > 25 or branches > 5 else "low"
163
+ methods.append({
164
+ "name": name, "line_count": line_count,
165
+ "complexity": complexity, "branches": branches,
166
+ })
167
+
168
+ if not methods and not classes:
169
+ return None
170
+
171
+ return {"file": filepath, "total_lines": total_lines, "methods": methods, "classes": classes}
@@ -0,0 +1,324 @@
1
+ """Tree-sitter based skeleton parser — multi-language AST extraction.
2
+
3
+ Wraps tree-sitter-languages to extract classes, methods, fields, imports,
4
+ and call targets from source files. Language-specific rules come from preset config.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from core.interfaces import SkeletonParser
13
+ from core.skeleton.parsers import register_parser
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Extension → tree-sitter language name
18
+ EXT_TO_LANG: dict[str, str] = {
19
+ ".java": "java", ".py": "python", ".go": "go",
20
+ ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "javascript",
21
+ ".kt": "kotlin", ".kts": "kotlin", ".scala": "scala", ".rs": "rust",
22
+ ".rb": "ruby", ".cs": "c_sharp", ".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp",
23
+ ".c": "c", ".h": "c", ".hpp": "cpp", ".swift": "swift", ".dart": "dart",
24
+ ".lua": "lua", ".php": "php", ".groovy": "groovy", ".r": "r", ".R": "r",
25
+ }
26
+
27
+ # tree-sitter node types for classes/structs per language
28
+ CLASS_NODES: dict[str, list[str]] = {
29
+ "java": ["class_declaration", "interface_declaration", "enum_declaration", "record_declaration"],
30
+ "python": ["class_definition"], "go": ["type_declaration"],
31
+ "typescript": ["class_declaration", "interface_declaration", "enum_declaration"],
32
+ "tsx": ["class_declaration", "interface_declaration", "enum_declaration"],
33
+ "javascript": ["class_declaration"],
34
+ "kotlin": ["class_declaration", "object_declaration", "interface_declaration"],
35
+ "scala": ["class_definition", "object_definition", "trait_definition"],
36
+ "rust": ["struct_item", "enum_item", "impl_item", "trait_item"],
37
+ "ruby": ["class", "module"],
38
+ "c_sharp": ["class_declaration", "interface_declaration", "enum_declaration",
39
+ "struct_declaration", "record_declaration"],
40
+ "cpp": ["class_specifier", "struct_specifier"], "c": ["struct_specifier"],
41
+ "swift": ["class_declaration", "struct_declaration", "protocol_declaration", "enum_declaration"],
42
+ "php": ["class_declaration", "interface_declaration", "enum_declaration"],
43
+ }
44
+
45
+ # tree-sitter node types for functions/methods per language
46
+ METHOD_NODES: dict[str, list[str]] = {
47
+ "java": ["method_declaration", "constructor_declaration"],
48
+ "python": ["function_definition"],
49
+ "go": ["function_declaration", "method_declaration"],
50
+ "typescript": ["function_declaration", "method_definition", "arrow_function"],
51
+ "tsx": ["function_declaration", "method_definition", "arrow_function"],
52
+ "javascript": ["function_declaration", "method_definition", "arrow_function"],
53
+ "kotlin": ["function_declaration"], "scala": ["function_definition", "val_definition"],
54
+ "rust": ["function_item"], "ruby": ["method", "singleton_method"],
55
+ "c_sharp": ["method_declaration", "constructor_declaration"],
56
+ "cpp": ["function_definition"], "c": ["function_definition"],
57
+ "swift": ["function_declaration"], "php": ["method_declaration", "function_definition"],
58
+ }
59
+
60
+ # Branch node types for complexity analysis
61
+ BRANCH_TYPES = frozenset({
62
+ "if_statement", "if_expression", "else_clause", "elif_clause",
63
+ "switch_statement", "switch_expression", "case_clause", "match_expression",
64
+ "match_arm", "for_statement", "for_expression", "while_statement",
65
+ "try_statement", "catch_clause", "except_clause",
66
+ "ternary_expression", "conditional_expression", "guard_statement",
67
+ })
68
+
69
+
70
+
71
+ def is_available() -> bool:
72
+ """Check if tree-sitter-languages is installed."""
73
+ try:
74
+ import tree_sitter_languages # noqa: F401
75
+ return True
76
+ except ImportError:
77
+ return False
78
+
79
+
80
+ def detect_language(filepath: str) -> str | None:
81
+ """Detect language from file extension."""
82
+ return EXT_TO_LANG.get(Path(filepath).suffix.lower())
83
+
84
+ @register_parser
85
+ class TreeSitterSkeletonParser(SkeletonParser):
86
+ """Tree-sitter skeleton parser. Registered as 'treesitter'."""
87
+
88
+ name = "treesitter"
89
+ priority = 80
90
+
91
+ def can_parse(self, repo_path: Path, preset: dict[str, Any]) -> bool:
92
+ return is_available()
93
+
94
+ def parse(self, repo_path: Path, preset: dict[str, Any], **kwargs: Any) -> list[dict[str, Any]]:
95
+ """Parse all source files in repo, returning skeleton entries."""
96
+ from core.git import ls_tree
97
+ ref = kwargs.get("ref", "HEAD")
98
+ subpath = kwargs.get("subpath")
99
+ parser_config = preset.get("parser_config", {})
100
+ extensions = parser_config.get("extensions") or list(EXT_TO_LANG.keys())
101
+ skip_dirs = set(parser_config.get("skip_dirs", ["test", "tests", "generated"]))
102
+ all_files = ls_tree(repo_path, ref)
103
+
104
+ subpath_prefix = ""
105
+ if subpath:
106
+ subpath_prefix = subpath.rstrip("/") + "/"
107
+ all_files = [f for f in all_files if f.startswith(subpath_prefix)]
108
+
109
+ entries: list[dict[str, Any]] = []
110
+ for filepath in all_files:
111
+ rel_path = filepath[len(subpath_prefix):] if subpath_prefix else filepath
112
+ if any(f"/{d}/" in rel_path or rel_path.startswith(f"{d}/") for d in skip_dirs):
113
+ continue
114
+ if not any(rel_path.endswith(ext) for ext in extensions):
115
+ continue
116
+ content = _read_file_content(repo_path, ref, filepath)
117
+ if content is None:
118
+ continue
119
+ entry = parse_file(content, rel_path)
120
+ if entry:
121
+ entries.append(entry)
122
+ return entries
123
+
124
+ def list_source_files(self, repo_path: Path, preset: dict[str, Any], ref: str = "HEAD", **kwargs: Any) -> list[str]:
125
+ """List source files (excluding test and generated files)."""
126
+ from core.git import ls_tree
127
+ subpath = kwargs.get("subpath")
128
+ parser_config = preset.get("parser_config", {})
129
+ extensions = parser_config.get("extensions") or list(EXT_TO_LANG.keys())
130
+ test_patterns = parser_config.get("test_patterns", [])
131
+ generated_patterns = parser_config.get("generated_patterns", [])
132
+ all_files = ls_tree(repo_path, ref)
133
+
134
+ subpath_prefix = ""
135
+ if subpath:
136
+ subpath_prefix = subpath.rstrip("/") + "/"
137
+ all_files = [f for f in all_files if f.startswith(subpath_prefix)]
138
+
139
+ result = []
140
+ for f in all_files:
141
+ rel_path = f[len(subpath_prefix):] if subpath_prefix else f
142
+ if any(p in rel_path for p in test_patterns):
143
+ continue
144
+ if any(p in rel_path for p in generated_patterns):
145
+ continue
146
+ if extensions and not any(rel_path.endswith(ext) for ext in extensions):
147
+ continue
148
+ result.append(rel_path)
149
+ return result
150
+
151
+
152
+ # ---------------------------------------------------------------------------
153
+ # Internal helpers
154
+ # ---------------------------------------------------------------------------
155
+ _parser_cache: dict[str, Any] = {}
156
+ _parser_lock = __import__("threading").Lock()
157
+
158
+ def _get_ts_parser(lang: str):
159
+ """Get or create a tree-sitter parser for a language."""
160
+ with _parser_lock:
161
+ if lang in _parser_cache:
162
+ return _parser_cache[lang]
163
+ try:
164
+ import tree_sitter_languages
165
+ parser = tree_sitter_languages.get_parser(lang)
166
+ with _parser_lock:
167
+ _parser_cache[lang] = parser
168
+ return parser
169
+ except (ImportError, Exception) as e:
170
+ logger.warning("No tree-sitter grammar for %s: %s", lang, e)
171
+ return None
172
+
173
+
174
+ def _read_file_content(repo_path: Path, ref: str, filepath: str) -> str | None:
175
+ """Read file content from git or filesystem."""
176
+ from core.git import read_file
177
+ try:
178
+ return read_file(repo_path, ref, filepath)
179
+ except Exception:
180
+ full = repo_path / filepath
181
+ if full.exists():
182
+ try:
183
+ return full.read_text(encoding="utf-8")
184
+ except (OSError, UnicodeDecodeError):
185
+ pass
186
+ return None
187
+
188
+
189
+ def parse_file(content: str, filepath: str) -> dict[str, Any] | None:
190
+ """Parse a single file into a skeleton entry. Public for direct use."""
191
+ from core.skeleton.parsers.treesitter_java import extract_call_targets
192
+ from core.skeleton.parsers.treesitter_multi import (
193
+ fallback_parse, extract_imports, extract_classes, extract_methods, extract_fields,
194
+ )
195
+ lang = detect_language(filepath)
196
+ if not lang:
197
+ return fallback_parse(content, filepath)
198
+ parser = _get_ts_parser(lang)
199
+ if not parser:
200
+ return fallback_parse(content, filepath)
201
+ try:
202
+ tree = parser.parse(content.encode("utf-8"))
203
+ except Exception as e:
204
+ logger.debug("Parse failed for %s: %s", filepath, e)
205
+ return fallback_parse(content, filepath)
206
+
207
+ root = tree.root_node
208
+ classes = extract_classes(root, lang)
209
+ methods = extract_methods(root, lang, content)
210
+ if not methods and not classes:
211
+ return None
212
+ entry: dict[str, Any] = {
213
+ "file": filepath, "total_lines": len(content.splitlines()),
214
+ "methods": methods, "classes": classes,
215
+ }
216
+ fields = extract_fields(root, lang)
217
+ if fields:
218
+ entry["fields"] = fields
219
+ imports = extract_imports(root, lang)
220
+ if imports:
221
+ entry["imports"] = imports
222
+ calls = extract_call_targets(root, lang, content)
223
+ if calls:
224
+ entry["calls"] = calls
225
+ return entry
226
+
227
+
228
+ # ---------------------------------------------------------------------------
229
+ # Common AST traversal utilities (used by treesitter_java and treesitter_multi)
230
+ # ---------------------------------------------------------------------------
231
+
232
+ def _walk_nodes(root, node_types: list[str]):
233
+ """Recursively yield nodes matching given types."""
234
+ stack = [root]
235
+ while stack:
236
+ node = stack.pop()
237
+ if node.type in node_types:
238
+ yield node
239
+ stack.extend(reversed(node.children))
240
+
241
+ def _get_name(node, lang: str) -> str | None:
242
+ """Extract name identifier from a declaration node."""
243
+ name_node = node.child_by_field_name("name")
244
+ if name_node:
245
+ return name_node.text.decode("utf-8")
246
+ for child in node.children:
247
+ if child.type in ("identifier", "type_identifier", "property_identifier"):
248
+ return child.text.decode("utf-8")
249
+ return None
250
+
251
+ def _classify_type(node, lang: str) -> str:
252
+ """Classify a class-like node into class/interface/enum/struct."""
253
+ _type_map = {
254
+ "interface_declaration": "interface", "enum_declaration": "enum",
255
+ "record_declaration": "record", "struct_item": "struct",
256
+ "struct_specifier": "struct", "struct_declaration": "struct",
257
+ "trait_item": "interface", "trait_definition": "interface",
258
+ "object_declaration": "object", "object_definition": "object",
259
+ "protocol_declaration": "interface", "module": "module",
260
+ }
261
+ return _type_map.get(node.type, "class")
262
+
263
+ def _get_access(node, lang: str) -> str:
264
+ """Extract access modifier."""
265
+ if lang in ("java", "c_sharp", "kotlin"):
266
+ for child in node.children:
267
+ if child.type == "modifiers":
268
+ text = child.text.decode("utf-8")
269
+ for mod in ("public", "protected", "private"):
270
+ if mod in text:
271
+ return mod
272
+ elif lang == "go":
273
+ name = _get_name(node, lang)
274
+ return "public" if name and name[0].isupper() else "private"
275
+ elif lang == "rust":
276
+ if any(c.type == "visibility_modifier" for c in node.children):
277
+ return "public"
278
+ return "private"
279
+ elif lang == "python":
280
+ name = _get_name(node, lang)
281
+ if name and name.startswith("__") and not name.endswith("__"):
282
+ return "private"
283
+ if name and name.startswith("_"):
284
+ return "protected"
285
+ return "public"
286
+
287
+ def _get_params(node, lang: str) -> str | None:
288
+ """Extract parameter string."""
289
+ params_node = (
290
+ node.child_by_field_name("parameters")
291
+ or node.child_by_field_name("formal_parameters")
292
+ )
293
+ if params_node:
294
+ text = params_node.text.decode("utf-8").strip("()")
295
+ return text[:200]
296
+ return None
297
+
298
+ def _get_return_type(node, lang: str) -> str | None:
299
+ """Extract return type."""
300
+ ret_node = node.child_by_field_name("return_type") or node.child_by_field_name("type")
301
+ if ret_node:
302
+ return ret_node.text.decode("utf-8")[:100]
303
+ return None
304
+
305
+ def _get_annotations(node, lang: str) -> list[str | dict]:
306
+ """Extract annotations/decorators."""
307
+ from core.skeleton.parsers.treesitter_java import parse_java_annotation
308
+
309
+ annotations: list[str | dict] = []
310
+ if lang in ("java", "kotlin", "c_sharp"):
311
+ for child in node.children:
312
+ if child.type in ("modifiers", "annotation"):
313
+ for sub in child.children:
314
+ if sub.type in ("marker_annotation", "annotation"):
315
+ annotations.append(parse_java_annotation(sub, lang))
316
+ prev = node.prev_sibling
317
+ while prev and prev.type in ("marker_annotation", "annotation"):
318
+ annotations.append(parse_java_annotation(prev, lang))
319
+ prev = prev.prev_sibling
320
+ elif lang == "python":
321
+ for child in node.children:
322
+ if child.type == "decorator":
323
+ annotations.append(child.text.decode("utf-8"))
324
+ return annotations[:10]