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,71 @@
1
+ """Skeleton parser registry.
2
+
3
+ Parsers are registered by name and selected based on preset configuration.
4
+ The parser chain tries each parser in priority order until one succeeds.
5
+
6
+ Usage:
7
+ from core.skeleton.parsers import get_parser_chain, register_parser
8
+
9
+ chain = get_parser_chain(preset)
10
+ for parser in chain:
11
+ if parser.can_parse(repo_path, preset):
12
+ entries = parser.parse(repo_path, preset)
13
+ break
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from core.interfaces import SkeletonParser
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ _REGISTRY: dict[str, type[SkeletonParser]] = {}
27
+
28
+
29
+ def register_parser(cls: type[SkeletonParser]) -> type[SkeletonParser]:
30
+ """Decorator to register a parser class by its `name` attribute."""
31
+ _REGISTRY[cls.name] = cls
32
+ return cls
33
+
34
+
35
+ def get_parser_chain(preset: dict[str, Any]) -> list[SkeletonParser]:
36
+ """Return parser instances in preset-declared priority order.
37
+
38
+ Falls back to ["treesitter", "regex"] if preset doesn't declare parsers.
39
+ Skips unknown parser names with a warning.
40
+ """
41
+ names = preset.get("parsers", ["treesitter", "regex"])
42
+ chain: list[SkeletonParser] = []
43
+ for name in names:
44
+ cls = _REGISTRY.get(name)
45
+ if cls is None:
46
+ logger.warning("Unknown parser '%s' in preset config, skipping", name)
47
+ continue
48
+ chain.append(cls())
49
+ return chain
50
+
51
+
52
+ def available_parsers() -> list[str]:
53
+ """Return names of all registered parsers."""
54
+ return sorted(_REGISTRY.keys())
55
+
56
+
57
+ def get_parser(name: str) -> SkeletonParser:
58
+ """Get a specific parser by name. Raises KeyError if not found."""
59
+ cls = _REGISTRY.get(name)
60
+ if cls is None:
61
+ raise KeyError(f"Parser '{name}' not registered. Available: {available_parsers()}")
62
+ return cls()
63
+
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Auto-import parser submodules to trigger @register_parser decorators.
67
+ # Without these imports, the decorator never executes and _REGISTRY stays empty.
68
+ # ---------------------------------------------------------------------------
69
+ from core.skeleton.parsers import treesitter # noqa: F401, E402
70
+ from core.skeleton.parsers import jqassistant # noqa: F401, E402
71
+ from core.skeleton.parsers import regex # noqa: F401, E402
@@ -0,0 +1,300 @@
1
+ """jQAssistant-based skeleton parser — Java bytecode analysis.
2
+
3
+ Uses jQAssistant + embedded Neo4j to extract precise class/method/field/annotation
4
+ data from compiled .class files. Falls back gracefully if unavailable.
5
+
6
+ Requires: jQAssistant CLI on PATH (or JQASSISTANT_HOME), Java 11+,
7
+ compiled .class files (target/classes or build/classes).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ import os
15
+ import re
16
+ import shutil
17
+ import subprocess
18
+ import tempfile
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ from core.interfaces import SkeletonParser
23
+ from core.skeleton.parsers import register_parser
24
+ from core.skeleton.parsers.jqassistant_cypher import (
25
+ CYPHER_ANNOTATIONS, CYPHER_CALL_GRAPH, CYPHER_CLASSES,
26
+ CYPHER_FIELDS, CYPHER_METHODS,
27
+ apply_annotations, apply_fields, apply_methods,
28
+ build_call_graph_edges, build_class_entries, parse_cypher_output,
29
+ )
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def is_available() -> bool:
35
+ """Check if jQAssistant CLI is available."""
36
+ return _find_jqa_command() is not None
37
+
38
+
39
+ def _find_jqa_command() -> str | None:
40
+ """Locate jQAssistant CLI executable."""
41
+ for cmd in ("jqassistant", "jqassistant.cmd", "jqassistant.bat"):
42
+ if shutil.which(cmd):
43
+ return cmd
44
+ jqa_home = os.environ.get("JQASSISTANT_HOME")
45
+ if jqa_home:
46
+ for suffix in ("bin/jqassistant", "bin/jqassistant.cmd", "bin/jqassistant.bat"):
47
+ full = os.path.join(jqa_home, suffix)
48
+ if os.path.isfile(full):
49
+ return full
50
+ return None
51
+
52
+
53
+ @register_parser
54
+ class JQAssistantParser(SkeletonParser):
55
+ """jQAssistant parser. Registered as 'jqassistant'. Java-only, highest priority."""
56
+
57
+ name = "jqassistant"
58
+ priority = 100
59
+
60
+ def can_parse(self, repo_path: Path, preset: dict[str, Any]) -> bool:
61
+ """Available only if jQAssistant CLI exists AND compiled classes are present."""
62
+ if not is_available():
63
+ return False
64
+ candidates = [
65
+ repo_path / "target" / "classes",
66
+ repo_path / "build" / "classes" / "java" / "main",
67
+ ]
68
+ # Multi-module: check sub-modules
69
+ for sub in repo_path.iterdir():
70
+ if sub.is_dir() and (sub / "pom.xml").exists():
71
+ candidates.append(sub / "target" / "classes")
72
+ return any(d.is_dir() and any(d.rglob("*.class")) for d in candidates)
73
+
74
+ def parse(self, repo_path: Path, preset: dict[str, Any], **kwargs: Any) -> list[dict[str, Any]]:
75
+ """Scan compiled classes and extract skeleton entries via Cypher queries."""
76
+ jqa_cmd = _find_jqa_command()
77
+ if not jqa_cmd:
78
+ return []
79
+
80
+ classes_dir = self._find_classes_dir(repo_path)
81
+ if not classes_dir:
82
+ return []
83
+
84
+ store_dir = tempfile.mkdtemp(prefix="jqa-")
85
+ try:
86
+ if not self._run_scan(jqa_cmd, store_dir, classes_dir):
87
+ return []
88
+ return self._query_all_skeleton(jqa_cmd, store_dir, repo_path)
89
+ finally:
90
+ shutil.rmtree(store_dir, ignore_errors=True)
91
+
92
+ # ------------------------------------------------------------------
93
+ # Internal helpers
94
+ # ------------------------------------------------------------------
95
+
96
+ def _find_classes_dir(self, repo_path: Path) -> Path | None:
97
+ """Find the compiled classes directory."""
98
+ candidates = [
99
+ repo_path / "target" / "classes",
100
+ repo_path / "build" / "classes" / "java" / "main",
101
+ ]
102
+ for d in candidates:
103
+ if d.is_dir() and any(d.rglob("*.class")):
104
+ return d
105
+ # Multi-module: find first sub-module with classes
106
+ for sub in sorted(repo_path.iterdir()):
107
+ if sub.is_dir() and (sub / "target" / "classes").is_dir():
108
+ return sub / "target" / "classes"
109
+ return None
110
+
111
+ def _run_scan(self, jqa_cmd: str, store_dir: str, classes_dir: Path) -> bool:
112
+ """Run jQAssistant scan on classes directory."""
113
+ try:
114
+ result = subprocess.run(
115
+ [jqa_cmd, "scan", "-f", str(classes_dir), "-s", store_dir],
116
+ capture_output=True, encoding="utf-8", errors="replace", timeout=300,
117
+ )
118
+ return result.returncode == 0
119
+ except (subprocess.TimeoutExpired, OSError) as e:
120
+ logger.warning("jQAssistant scan failed: %s", e)
121
+ return False
122
+
123
+ def _query_all_skeleton(
124
+ self, jqa_cmd: str, store_dir: str, repo_path: Path
125
+ ) -> list[dict[str, Any]]:
126
+ """Query Neo4j store for all skeleton data and assemble entries."""
127
+ classes_data = self._run_cypher(jqa_cmd, store_dir, repo_path, CYPHER_CLASSES)
128
+ methods_data = self._run_cypher(jqa_cmd, store_dir, repo_path, CYPHER_METHODS)
129
+ fields_data = self._run_cypher(jqa_cmd, store_dir, repo_path, CYPHER_FIELDS)
130
+ annotations_data = self._run_cypher(jqa_cmd, store_dir, repo_path, CYPHER_ANNOTATIONS)
131
+
132
+ cache = build_class_entries(classes_data)
133
+ apply_annotations(cache, annotations_data)
134
+ apply_methods(cache, methods_data)
135
+ apply_fields(cache, fields_data)
136
+
137
+ return list(cache.values())
138
+
139
+ def _run_cypher(
140
+ self, jqa_cmd: str, store_dir: str, repo_path: Path, query: str
141
+ ) -> list[dict]:
142
+ """Execute a Cypher query against the jQAssistant store."""
143
+ with tempfile.NamedTemporaryFile(
144
+ mode="w", suffix=".cypher", delete=False
145
+ ) as f:
146
+ f.write(query)
147
+ query_file = f.name
148
+
149
+ try:
150
+ cmd = [
151
+ jqa_cmd, "report",
152
+ "-s", store_dir,
153
+ "-concepts", query_file,
154
+ "-reportFormat", "json",
155
+ ]
156
+ result = subprocess.run(
157
+ cmd,
158
+ capture_output=True,
159
+ encoding="utf-8",
160
+ errors="replace",
161
+ timeout=120,
162
+ cwd=str(repo_path),
163
+ )
164
+ if result.returncode != 0:
165
+ logger.warning("Cypher query failed: %s", result.stderr[:500])
166
+ return []
167
+
168
+ try:
169
+ data = json.loads(result.stdout)
170
+ return data if isinstance(data, list) else data.get("rows", [])
171
+ except json.JSONDecodeError:
172
+ return parse_cypher_output(result.stdout)
173
+ except (subprocess.TimeoutExpired, OSError) as e:
174
+ logger.warning("Cypher query error: %s", e)
175
+ return []
176
+ finally:
177
+ os.unlink(query_file)
178
+
179
+
180
+ # ------------------------------------------------------------------
181
+ # Source-only fallback parsing (when jQAssistant scan is unavailable)
182
+ # ------------------------------------------------------------------
183
+
184
+ _CLASS_PATTERN = re.compile(
185
+ r'(?:public|protected|private)?\s*(?:abstract|final|static)?\s*'
186
+ r'(class|interface|enum|record)\s+(\w+)'
187
+ )
188
+
189
+ _METHOD_PATTERN = re.compile(
190
+ r'(?:public|protected|private|static|\s)+[\w<>\[\],\s]+\s+(\w+)\s*\('
191
+ )
192
+
193
+ _CONSTANT_PATTERN = re.compile(
194
+ r'(?:public|private|protected)?\s*static\s+final\s+'
195
+ r'(\w+)\s+([A-Z_][A-Z0-9_]*)\s*=\s*(.+?)\s*;'
196
+ )
197
+
198
+
199
+ def parse_source_only(content: str, filepath: str) -> dict[str, Any] | None:
200
+ """Fallback: extract basic skeleton from source when jQAssistant data unavailable."""
201
+ lines = content.split("\n")
202
+ if not lines:
203
+ return None
204
+ entry: dict[str, Any] = {
205
+ "file": filepath, "total_lines": len(lines),
206
+ "classes": [], "methods": [], "fields": [],
207
+ }
208
+
209
+ for line in lines:
210
+ m = _CLASS_PATTERN.search(line)
211
+ if m:
212
+ entry["classes"].append({
213
+ "name": m.group(2),
214
+ "type": m.group(1),
215
+ "annotations": [],
216
+ })
217
+
218
+ method_metrics = _extract_method_line_counts(content)
219
+ for name, metrics in method_metrics.items():
220
+ entry["methods"].append({
221
+ "name": name, "access": "public", "static": False,
222
+ "return_type": "", "params": "", "annotations": [],
223
+ "line_count": metrics["line_count"],
224
+ "complexity": metrics["complexity"],
225
+ "branches": metrics["branches"], "constants": [],
226
+ })
227
+
228
+ return entry if entry["classes"] or entry["methods"] else None
229
+
230
+
231
+ def enrich_with_source_metrics(entry: dict[str, Any], content: str) -> None:
232
+ """Enrich jQAssistant entry with source-level metrics (line counts, complexity)."""
233
+ entry["total_lines"] = len(content.split("\n"))
234
+ method_lines = _extract_method_line_counts(content)
235
+ for method in entry.get("methods", []):
236
+ name = method.get("name", "")
237
+ if name in method_lines:
238
+ method["line_count"] = method_lines[name]["line_count"]
239
+ method["complexity"] = method_lines[name]["complexity"]
240
+ method["branches"] = method_lines[name]["branches"]
241
+ constants = _extract_constants(content)
242
+ if constants:
243
+ entry.setdefault("constants", []).extend(constants)
244
+
245
+
246
+ def _extract_method_line_counts(content: str) -> dict[str, dict]:
247
+ """Extract method line counts and complexity from source."""
248
+ results: dict[str, dict] = {}
249
+ lines = content.split("\n")
250
+ branch_keywords = (
251
+ "if ", "if(", "else ", "for ", "for(", "while ", "while(",
252
+ "switch ", "switch(", "catch ", "catch(", "case ",
253
+ )
254
+
255
+ i = 0
256
+ while i < len(lines):
257
+ match = _METHOD_PATTERN.search(lines[i])
258
+ if match:
259
+ name = match.group(1)
260
+ start = i
261
+ brace_count = 0
262
+ found_open = False
263
+ branches = 0
264
+
265
+ for j in range(i, len(lines)):
266
+ line = lines[j]
267
+ brace_count += line.count("{") - line.count("}")
268
+ if "{" in line:
269
+ found_open = True
270
+ if found_open and brace_count <= 0:
271
+ line_count = j - start + 1
272
+ complexity = (
273
+ "low" if branches <= 2
274
+ else ("medium" if branches <= 5 else "high")
275
+ )
276
+ results[name] = {
277
+ "line_count": line_count,
278
+ "complexity": complexity,
279
+ "branches": branches,
280
+ }
281
+ i = j
282
+ break
283
+
284
+ if any(kw in line for kw in branch_keywords):
285
+ branches += 1
286
+ i += 1
287
+
288
+ return results
289
+
290
+
291
+ def _extract_constants(content: str) -> list[dict[str, str]]:
292
+ """Extract static final constants from source."""
293
+ constants: list[dict[str, str]] = []
294
+ for match in _CONSTANT_PATTERN.finditer(content):
295
+ constants.append({
296
+ "name": match.group(2),
297
+ "type": match.group(1),
298
+ "value": match.group(3)[:100],
299
+ })
300
+ return constants
@@ -0,0 +1,225 @@
1
+ """Cypher query templates and data transformation for jQAssistant parser.
2
+
3
+ Contains all Cypher queries used to extract skeleton data from jQAssistant's
4
+ embedded Neo4j graph database, plus helper functions that transform raw query
5
+ results into the skeleton entry format expected by the parser chain.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ # ============================================================
13
+ # Cypher Query Templates
14
+ # ============================================================
15
+
16
+ CYPHER_CLASSES = """
17
+ MATCH (t:Type:Class)
18
+ WHERE NOT t:Inner
19
+ RETURN t.fqn AS fqn, t.name AS name, 'class' AS type,
20
+ COALESCE(t.superClass, '') AS superClass,
21
+ [(t)-[:IMPLEMENTS]->(i) | i.fqn] AS interfaces
22
+ UNION ALL
23
+ MATCH (t:Type:Interface)
24
+ RETURN t.fqn AS fqn, t.name AS name, 'interface' AS type,
25
+ '' AS superClass, [] AS interfaces
26
+ UNION ALL
27
+ MATCH (t:Type:Enum)
28
+ RETURN t.fqn AS fqn, t.name AS name, 'enum' AS type,
29
+ '' AS superClass, [] AS interfaces
30
+ """
31
+
32
+ CYPHER_METHODS = """
33
+ MATCH (t:Type)-[:DECLARES]->(m:Method)
34
+ WHERE NOT m.name STARTS WITH '<' AND NOT t:Inner
35
+ RETURN t.fqn AS classFqn, m.name AS name,
36
+ m.visibility AS visibility,
37
+ m.static AS static,
38
+ COALESCE(m.returnType, 'void') AS returnType,
39
+ [(m)-[:HAS]->(p:Parameter) | p.type + ' ' + p.name] AS params,
40
+ [(m)-[:ANNOTATED_BY]->(a) | '@' + last(split(a.fqn, '.'))] AS annotations
41
+ """
42
+
43
+ CYPHER_FIELDS = """
44
+ MATCH (t:Type)-[:DECLARES]->(f:Field)
45
+ WHERE NOT t:Inner
46
+ RETURN t.fqn AS classFqn, f.name AS name,
47
+ COALESCE(f.type, '') AS type,
48
+ [(f)-[:ANNOTATED_BY]->(a) | '@' + last(split(a.fqn, '.'))] AS annotations
49
+ """
50
+
51
+ CYPHER_ANNOTATIONS = """
52
+ MATCH (t:Type)-[:ANNOTATED_BY]->(a:Annotation)-[:OF_TYPE]->(at:Type)
53
+ WHERE NOT t:Inner
54
+ RETURN t.fqn AS classFqn, t.name AS className,
55
+ at.fqn AS annotationType,
56
+ 'class' AS target,
57
+ '' AS methodName,
58
+ COALESCE(a.value, '') AS value
59
+ UNION ALL
60
+ MATCH (t:Type)-[:DECLARES]->(m:Method)-[:ANNOTATED_BY]->(a:Annotation)-[:OF_TYPE]->(at:Type)
61
+ WHERE NOT t:Inner AND NOT m.name STARTS WITH '<'
62
+ RETURN t.fqn AS classFqn, t.name AS className,
63
+ at.fqn AS annotationType,
64
+ 'method' AS target,
65
+ m.name AS methodName,
66
+ COALESCE(a.value, '') AS value
67
+ """
68
+
69
+ CYPHER_CALL_GRAPH = """
70
+ MATCH (caller:Type)-[:DECLARES]->(m:Method)-[:INVOKES]->(callee_m:Method)<-[:DECLARES]-(callee:Type)
71
+ WHERE NOT caller:Inner AND NOT callee:Inner
72
+ AND caller <> callee
73
+ AND NOT m.name STARTS WITH '<'
74
+ AND NOT callee_m.name STARTS WITH '<'
75
+ RETURN caller.fqn AS callerClass, callee.fqn AS calleeClass,
76
+ callee_m.name AS calleeMethod
77
+ ORDER BY callerClass, calleeClass
78
+ """
79
+
80
+ # All queries grouped for convenience
81
+ CYPHER_QUERIES = {
82
+ "classes": CYPHER_CLASSES,
83
+ "methods": CYPHER_METHODS,
84
+ "fields": CYPHER_FIELDS,
85
+ "annotations": CYPHER_ANNOTATIONS,
86
+ "call_graph": CYPHER_CALL_GRAPH,
87
+ }
88
+
89
+
90
+ # ============================================================
91
+ # Data Transformation Functions
92
+ # ============================================================
93
+
94
+
95
+ def fqn_to_source_path(fqn: str) -> str:
96
+ """Convert Java FQN to source file path.
97
+
98
+ com.example.service.MyService → src/main/java/com/example/service/MyService.java
99
+ """
100
+ if not fqn:
101
+ return ""
102
+ parts = fqn.replace(".", "/")
103
+ if "$" in parts:
104
+ parts = parts.split("$")[0]
105
+ return f"src/main/java/{parts}.java"
106
+
107
+
108
+ def build_class_entries(classes_data: list[dict]) -> dict[str, dict[str, Any]]:
109
+ """Transform raw class query results into skeleton cache entries.
110
+
111
+ Returns a dict keyed by source file path.
112
+ """
113
+ cache: dict[str, dict[str, Any]] = {}
114
+ for row in classes_data:
115
+ fqn = row.get("fqn", "")
116
+ source_file = fqn_to_source_path(fqn)
117
+ if not source_file:
118
+ continue
119
+ if source_file not in cache:
120
+ cache[source_file] = {
121
+ "file": source_file,
122
+ "total_lines": 0,
123
+ "classes": [],
124
+ "methods": [],
125
+ "fields": [],
126
+ }
127
+ cache[source_file]["classes"].append({
128
+ "name": row.get("name", ""),
129
+ "type": row.get("type", "class").lower(),
130
+ "annotations": [],
131
+ "extends": row.get("superClass", ""),
132
+ "implements": row.get("interfaces", []),
133
+ })
134
+ return cache
135
+
136
+
137
+ def apply_annotations(cache: dict[str, dict], annotations_data: list[dict]) -> None:
138
+ """Apply annotation data to classes and methods in the cache (mutates in place)."""
139
+ for row in annotations_data:
140
+ source_file = fqn_to_source_path(row.get("classFqn", ""))
141
+ if source_file not in cache:
142
+ continue
143
+ anno_name = "@" + row.get("annotationType", "").split(".")[-1]
144
+ anno_value = row.get("value", "")
145
+ anno_str = f"{anno_name}({anno_value})" if anno_value else anno_name
146
+
147
+ target = row.get("target", "class")
148
+ if target == "class":
149
+ for cls in cache[source_file]["classes"]:
150
+ if cls["name"] == row.get("className", ""):
151
+ cls["annotations"].append(anno_str)
152
+ break
153
+ elif target == "method":
154
+ for m in cache[source_file]["methods"]:
155
+ if m["name"] == row.get("methodName", ""):
156
+ m.setdefault("annotations", []).append(anno_str)
157
+ break
158
+
159
+
160
+ def apply_methods(cache: dict[str, dict], methods_data: list[dict]) -> None:
161
+ """Apply method data to the cache (mutates in place)."""
162
+ for row in methods_data:
163
+ source_file = fqn_to_source_path(row.get("classFqn", ""))
164
+ if source_file not in cache:
165
+ continue
166
+ cache[source_file]["methods"].append({
167
+ "name": row.get("name", ""),
168
+ "access": row.get("visibility", "public").lower(),
169
+ "static": row.get("static", False),
170
+ "return_type": row.get("returnType", "void"),
171
+ "params": row.get("params", ""),
172
+ "annotations": row.get("annotations", []),
173
+ "line_count": 0,
174
+ "complexity": "low",
175
+ "branches": 0,
176
+ "constants": [],
177
+ })
178
+
179
+
180
+ def apply_fields(cache: dict[str, dict], fields_data: list[dict]) -> None:
181
+ """Apply field data to the cache (mutates in place)."""
182
+ for row in fields_data:
183
+ source_file = fqn_to_source_path(row.get("classFqn", ""))
184
+ if source_file not in cache:
185
+ continue
186
+ cache[source_file]["fields"].append({
187
+ "name": row.get("name", ""),
188
+ "type": row.get("type", "").split(".")[-1],
189
+ "annotations": row.get("annotations", []),
190
+ })
191
+
192
+
193
+ def build_call_graph_edges(results: list[dict]) -> list[dict[str, Any]]:
194
+ """Aggregate call graph results into caller-callee edge list.
195
+
196
+ Returns: [{"caller": "OrderService", "callee": "PaymentService", "methods": ["pay"]}]
197
+ """
198
+ edges: dict[tuple[str, str], list[str]] = {}
199
+ for row in results:
200
+ caller = row.get("callerClass", "").split(".")[-1]
201
+ callee = row.get("calleeClass", "").split(".")[-1]
202
+ method = row.get("calleeMethod", "")
203
+ if caller and callee and caller != callee:
204
+ key = (caller, callee)
205
+ edges.setdefault(key, []).append(method)
206
+
207
+ return [
208
+ {"caller": k[0], "callee": k[1], "methods": sorted(set(v))[:10]}
209
+ for k, v in edges.items()
210
+ ]
211
+
212
+
213
+ def parse_cypher_output(output: str) -> list[dict]:
214
+ """Parse non-JSON Cypher output (tab-separated or similar)."""
215
+ rows: list[dict] = []
216
+ lines = output.strip().split("\n")
217
+ if len(lines) < 2:
218
+ return rows
219
+
220
+ headers = [h.strip() for h in lines[0].split("\t")]
221
+ for line in lines[1:]:
222
+ values = [v.strip() for v in line.split("\t")]
223
+ if len(values) == len(headers):
224
+ rows.append(dict(zip(headers, values)))
225
+ return rows