@guanlin4924/devflow-pi 0.1.76 → 0.1.78
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.
- package/package.json +9 -2
- package/plugins/devflow-intel/skills/workflow-code-index/references/adapter-contract.md +8 -0
- package/plugins/devflow-intel/skills/workflow-code-index/references/data-model.md +1 -1
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/base.py +20 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/capabilities.py +26 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/generic_text.py +4 -2
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/go_ast.py +20 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/helpers/DevflowJavaAstHelper.java +96 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/helpers/go_ast_helper.go +96 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/helpers/php_ast_helper.php +91 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/helpers/tree_sitter_wasm_helper.mjs +50 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/helpers/typescript_ast_helper.cjs +123 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/java_ast.py +37 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/native_process.py +328 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/parser-capabilities.json +491 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/php_ast.py +55 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/python_ast.py +10 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/registry.py +55 -50
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/structured_scope.py +1 -1
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/tree_sitter_wasm.py +253 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/typescript_ast.py +31 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +29 -12
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/semantic.py +93 -12
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/writers/sqlite_writer.py +26 -13
- package/src/constants.ts +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Tuple
|
|
6
|
+
|
|
7
|
+
from adapters.native_process import HELPER_DIR, NativeProcessAdapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class JavaAstAdapter(NativeProcessAdapter):
|
|
11
|
+
name = "javac-tree-ast"
|
|
12
|
+
languages = ("java",)
|
|
13
|
+
native_description = "JDK javac com.sun.source Tree API; declarations, imports, and calls"
|
|
14
|
+
transport = "base64-tsv"
|
|
15
|
+
|
|
16
|
+
def __init__(self, fallback):
|
|
17
|
+
super().__init__(fallback)
|
|
18
|
+
self.classes = Path(tempfile.gettempdir()) / "devflow-java-ast-helper-v1"
|
|
19
|
+
|
|
20
|
+
def version(self) -> str:
|
|
21
|
+
return "1"
|
|
22
|
+
|
|
23
|
+
def check_available(self, codebase=None) -> Tuple[bool, str]:
|
|
24
|
+
javac, java = shutil.which("javac"), shutil.which("java")
|
|
25
|
+
if not javac or not java:
|
|
26
|
+
return False, "JDK javac/java executables not found"
|
|
27
|
+
source = HELPER_DIR / "DevflowJavaAstHelper.java"
|
|
28
|
+
target = self.classes / "DevflowJavaAstHelper.class"
|
|
29
|
+
if not target.exists() or target.stat().st_mtime < source.stat().st_mtime:
|
|
30
|
+
self.classes.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
result = subprocess.run([javac, "-d", str(self.classes), str(source)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False)
|
|
32
|
+
if result.returncode != 0:
|
|
33
|
+
return False, f"javac Tree API helper compilation failed: {result.stderr.strip()[:500]}"
|
|
34
|
+
return True, "JDK javac Tree API available"
|
|
35
|
+
|
|
36
|
+
def command(self, codebase=None) -> List[str]:
|
|
37
|
+
return [shutil.which("java") or "java", "-cp", str(self.classes), "DevflowJavaAstHelper"]
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import select
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, Iterable, List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from adapters.base import Adapter, ParseResult
|
|
11
|
+
from adapters.generic_text import _relation, _symbol
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
HELPER_DIR = Path(__file__).resolve().parent / "helpers"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NativeProcessAdapter(Adapter):
|
|
18
|
+
"""Base class for language-native parsers exposed through JSONL helpers.
|
|
19
|
+
|
|
20
|
+
Helpers stay alive for the lifetime of one index scan, which avoids the
|
|
21
|
+
process-startup cost for every source file. They return syntax facts only;
|
|
22
|
+
this class converts those facts into the stable shared index schema.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
languages: Tuple[str, ...] = ()
|
|
26
|
+
native_description = "language-native syntax parser"
|
|
27
|
+
transport = "json"
|
|
28
|
+
|
|
29
|
+
def __init__(self, fallback: Adapter):
|
|
30
|
+
self.fallback = fallback
|
|
31
|
+
self._processes: Dict[str, subprocess.Popen] = {}
|
|
32
|
+
self._availability: Dict[str, Tuple[bool, str]] = {}
|
|
33
|
+
|
|
34
|
+
def detect(self, codebase, files, manifests) -> int:
|
|
35
|
+
return 120 if any(self._language_for_path(path) in self.languages for path in files) else 0
|
|
36
|
+
|
|
37
|
+
def supports(self, language: str) -> bool:
|
|
38
|
+
return language in self.languages
|
|
39
|
+
|
|
40
|
+
def check_available(self, codebase=None) -> Tuple[bool, str]:
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
|
|
43
|
+
def command(self, codebase=None) -> List[str]:
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
|
|
46
|
+
def _availability_for(self, codebase=None) -> Tuple[bool, str]:
|
|
47
|
+
key = self._availability_key(codebase)
|
|
48
|
+
if key not in self._availability:
|
|
49
|
+
self._availability[key] = self.check_available(codebase)
|
|
50
|
+
return self._availability[key]
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def _availability_key(codebase=None) -> str:
|
|
54
|
+
if codebase is None:
|
|
55
|
+
return "<global>"
|
|
56
|
+
root = getattr(codebase, "root", None)
|
|
57
|
+
if root is not None:
|
|
58
|
+
try:
|
|
59
|
+
return str(Path(root).resolve())
|
|
60
|
+
except OSError:
|
|
61
|
+
return str(root)
|
|
62
|
+
return f"<codebase:{getattr(codebase, 'name', id(codebase))}>"
|
|
63
|
+
|
|
64
|
+
def parser_for_file(self, codebase, file_record) -> str:
|
|
65
|
+
available, _ = self._availability_for(codebase)
|
|
66
|
+
return self.parser_id if available else self.fallback.parser_for_file(codebase, file_record)
|
|
67
|
+
|
|
68
|
+
def capability(self) -> Dict:
|
|
69
|
+
codebase_results = [value for key, value in self._availability.items() if key != "<global>"]
|
|
70
|
+
if codebase_results:
|
|
71
|
+
available = any(value[0] for value in codebase_results)
|
|
72
|
+
reasons = sorted({value[1] for value in codebase_results})
|
|
73
|
+
reason = "; ".join(reasons)
|
|
74
|
+
else:
|
|
75
|
+
available, reason = self._availability_for(None)
|
|
76
|
+
return {
|
|
77
|
+
"parser": self.parser_id,
|
|
78
|
+
"languages": list(self.languages),
|
|
79
|
+
"native": True,
|
|
80
|
+
"available": available,
|
|
81
|
+
"reason": reason,
|
|
82
|
+
"fallback": self.fallback.parser_id,
|
|
83
|
+
"scope": self.native_description,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
def parse_file(self, codebase, file_record, text) -> ParseResult:
|
|
87
|
+
if not self.supports(file_record["language"]):
|
|
88
|
+
return self.fallback.parse_file(codebase, file_record, text)
|
|
89
|
+
available, reason = self._availability_for(codebase)
|
|
90
|
+
if not available:
|
|
91
|
+
return self._fallback_result(codebase, file_record, text, reason)
|
|
92
|
+
try:
|
|
93
|
+
payload = self._request(codebase, file_record, text)
|
|
94
|
+
except Exception as error:
|
|
95
|
+
self._stop_process(codebase)
|
|
96
|
+
return self._fallback_result(codebase, file_record, text, f"native parser failed: {error}", kind="parse_failed")
|
|
97
|
+
if not payload.get("ok", False):
|
|
98
|
+
message = payload.get("error") or "native parser returned no result"
|
|
99
|
+
return self._fallback_result(codebase, file_record, text, message, kind="parse_failed")
|
|
100
|
+
return self._convert(codebase, file_record, text, payload)
|
|
101
|
+
|
|
102
|
+
def _fallback_result(self, codebase, file_record, text, reason: str, kind: str = "adapter_missing") -> ParseResult:
|
|
103
|
+
result = self.fallback.parse_file(codebase, file_record, text)
|
|
104
|
+
result.actual_parser = result.actual_parser or self.fallback.parser_for_file(codebase, file_record)
|
|
105
|
+
result.diagnostics.append({
|
|
106
|
+
"codebase": codebase.name,
|
|
107
|
+
"path": file_record["path"],
|
|
108
|
+
"line": 1,
|
|
109
|
+
"severity": "warning",
|
|
110
|
+
"kind": kind,
|
|
111
|
+
"message": f"native AST unavailable for {file_record['language']}; {self.fallback.parser_id} fallback used: {reason}",
|
|
112
|
+
"parser": self.parser_id,
|
|
113
|
+
"metadata": {
|
|
114
|
+
"capability": "native_unavailable",
|
|
115
|
+
"native_parser": self.parser_id,
|
|
116
|
+
"fallback_parser": self.fallback.parser_id,
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
def _start_process(self, codebase=None) -> subprocess.Popen:
|
|
122
|
+
command = self.command(codebase)
|
|
123
|
+
key = self._process_key(codebase, command)
|
|
124
|
+
current = self._processes.get(key)
|
|
125
|
+
if current is not None and current.poll() is None:
|
|
126
|
+
return current
|
|
127
|
+
process = subprocess.Popen(
|
|
128
|
+
command,
|
|
129
|
+
stdin=subprocess.PIPE,
|
|
130
|
+
stdout=subprocess.PIPE,
|
|
131
|
+
stderr=subprocess.DEVNULL,
|
|
132
|
+
text=True,
|
|
133
|
+
encoding="utf-8",
|
|
134
|
+
bufsize=1,
|
|
135
|
+
env={**os.environ, "NO_COLOR": "1"},
|
|
136
|
+
)
|
|
137
|
+
self._processes[key] = process
|
|
138
|
+
return process
|
|
139
|
+
|
|
140
|
+
def _process_key(self, codebase=None, command: Optional[List[str]] = None) -> str:
|
|
141
|
+
actual_command = command if command is not None else self.command(codebase)
|
|
142
|
+
return f"{self._availability_key(codebase)}\0{json.dumps(actual_command, ensure_ascii=False)}"
|
|
143
|
+
|
|
144
|
+
def _request(self, codebase, file_record, text) -> Dict:
|
|
145
|
+
process = self._start_process(codebase)
|
|
146
|
+
if self.transport == "base64-tsv":
|
|
147
|
+
request = "\t".join([
|
|
148
|
+
base64.b64encode(file_record["path"].encode("utf-8")).decode("ascii"),
|
|
149
|
+
base64.b64encode(text.encode("utf-8")).decode("ascii"),
|
|
150
|
+
])
|
|
151
|
+
else:
|
|
152
|
+
request = json.dumps({"path": file_record["path"], "source": text}, ensure_ascii=False)
|
|
153
|
+
assert process.stdin is not None and process.stdout is not None
|
|
154
|
+
process.stdin.write(request + "\n")
|
|
155
|
+
process.stdin.flush()
|
|
156
|
+
readable, _, _ = select.select([process.stdout], [], [], 30.0)
|
|
157
|
+
if not readable:
|
|
158
|
+
raise TimeoutError("native parser timed out after 30 seconds")
|
|
159
|
+
response = process.stdout.readline()
|
|
160
|
+
if not response:
|
|
161
|
+
raise RuntimeError(f"native parser exited with status {process.poll()}")
|
|
162
|
+
return json.loads(response)
|
|
163
|
+
|
|
164
|
+
def _convert(self, codebase, record, text: str, payload: Dict) -> ParseResult:
|
|
165
|
+
path = record["path"]
|
|
166
|
+
language = record["language"]
|
|
167
|
+
content_hash = record["content_hash"]
|
|
168
|
+
file_symbol = _symbol(
|
|
169
|
+
codebase.name, language, "file", Path(path).name, path, 1,
|
|
170
|
+
max(1, len(text.splitlines())), content_hash,
|
|
171
|
+
qualified_name=path, parser=self.parser_id,
|
|
172
|
+
)
|
|
173
|
+
symbols = [file_symbol]
|
|
174
|
+
by_key: Dict[str, Dict] = {"": file_symbol}
|
|
175
|
+
pending = list(payload.get("declarations") or [])
|
|
176
|
+
# Helpers emit parents before children, but tolerate out-of-order data.
|
|
177
|
+
while pending:
|
|
178
|
+
remaining = []
|
|
179
|
+
progressed = False
|
|
180
|
+
for declaration in pending:
|
|
181
|
+
parent_key = str(declaration.get("parent") or "")
|
|
182
|
+
parent = by_key.get(parent_key)
|
|
183
|
+
if parent is None:
|
|
184
|
+
remaining.append(declaration)
|
|
185
|
+
continue
|
|
186
|
+
kind = declaration.get("kind") or "function"
|
|
187
|
+
name = declaration.get("name") or "anonymous"
|
|
188
|
+
signature = declaration.get("signature") or ""
|
|
189
|
+
qualified = declaration.get("qualified_name") or f"{parent['qualified_name']}::{name}"
|
|
190
|
+
symbol = _symbol(
|
|
191
|
+
codebase.name, language, kind, name, path,
|
|
192
|
+
max(1, int(declaration.get("line_start") or 1)),
|
|
193
|
+
max(1, int(declaration.get("line_end") or declaration.get("line_start") or 1)),
|
|
194
|
+
content_hash, qualified_name=qualified, signature=signature[:500],
|
|
195
|
+
parser=self.parser_id, visibility=declaration.get("visibility") or "",
|
|
196
|
+
)
|
|
197
|
+
symbol["container_id"] = parent["id"]
|
|
198
|
+
symbol["metadata"].update({"scope_resolution": "native_ast", "native_kind": declaration.get("native_kind", "")})
|
|
199
|
+
symbols.append(symbol)
|
|
200
|
+
by_key[str(declaration.get("key"))] = symbol
|
|
201
|
+
progressed = True
|
|
202
|
+
if not progressed:
|
|
203
|
+
# Malformed helper output must not create dangling containers.
|
|
204
|
+
for declaration in remaining:
|
|
205
|
+
declaration["parent"] = ""
|
|
206
|
+
pending = remaining
|
|
207
|
+
if pending and all(str(item.get("parent") or "") == "" for item in pending):
|
|
208
|
+
continue
|
|
209
|
+
break
|
|
210
|
+
pending = remaining
|
|
211
|
+
|
|
212
|
+
relations = []
|
|
213
|
+
for key, symbol in by_key.items():
|
|
214
|
+
if not key:
|
|
215
|
+
continue
|
|
216
|
+
relations.append(_relation(
|
|
217
|
+
symbol["container_id"] or file_symbol["id"], "contains", path,
|
|
218
|
+
symbol["line_start"], self.parser_id, "exact", to_id=symbol["id"],
|
|
219
|
+
metadata={"extraction": "native_ast"},
|
|
220
|
+
))
|
|
221
|
+
for item in payload.get("relations") or []:
|
|
222
|
+
owner = by_key.get(str(item.get("owner") or ""), file_symbol)
|
|
223
|
+
target_key = str(item.get("target_key") or "")
|
|
224
|
+
target = by_key.get(target_key) if target_key else None
|
|
225
|
+
relations.append(_relation(
|
|
226
|
+
owner["id"], item.get("type") or "calls", path,
|
|
227
|
+
max(1, int(item.get("line") or 1)), self.parser_id,
|
|
228
|
+
item.get("confidence") or "heuristic",
|
|
229
|
+
to_id=target["id"] if target else "",
|
|
230
|
+
to_external="" if target else str(item.get("target") or ""),
|
|
231
|
+
metadata={"extraction": "native_ast", **(item.get("metadata") or {})},
|
|
232
|
+
))
|
|
233
|
+
diagnostics = []
|
|
234
|
+
for item in payload.get("diagnostics") or []:
|
|
235
|
+
diagnostics.append({
|
|
236
|
+
"codebase": codebase.name,
|
|
237
|
+
"path": path,
|
|
238
|
+
"line": max(1, int(item.get("line") or 1)),
|
|
239
|
+
"severity": item.get("severity") or "warning",
|
|
240
|
+
"kind": item.get("kind") or "parse_failed",
|
|
241
|
+
"message": item.get("message") or "native parser diagnostic",
|
|
242
|
+
"parser": self.parser_id,
|
|
243
|
+
})
|
|
244
|
+
return ParseResult(symbols=symbols, relations=relations, diagnostics=diagnostics, actual_parser=self.parser_id)
|
|
245
|
+
|
|
246
|
+
def _stop_process(self, codebase=None) -> None:
|
|
247
|
+
if codebase is None:
|
|
248
|
+
processes = list(self._processes.values())
|
|
249
|
+
self._processes.clear()
|
|
250
|
+
else:
|
|
251
|
+
prefix = f"{self._availability_key(codebase)}\0"
|
|
252
|
+
keys = [key for key in self._processes if key.startswith(prefix)]
|
|
253
|
+
processes = [self._processes.pop(key) for key in keys]
|
|
254
|
+
for process in processes:
|
|
255
|
+
self._close_process(process)
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _close_process(process: subprocess.Popen) -> None:
|
|
259
|
+
try:
|
|
260
|
+
if process.stdin:
|
|
261
|
+
process.stdin.close()
|
|
262
|
+
process.terminate()
|
|
263
|
+
process.wait(timeout=1)
|
|
264
|
+
except Exception:
|
|
265
|
+
process.kill()
|
|
266
|
+
finally:
|
|
267
|
+
if process.stdout:
|
|
268
|
+
process.stdout.close()
|
|
269
|
+
|
|
270
|
+
def __del__(self):
|
|
271
|
+
self._stop_process()
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def _language_for_path(path: Path) -> str:
|
|
275
|
+
from adapters.generic_text import language_for_path
|
|
276
|
+
return language_for_path(path)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def executable(name: str) -> Tuple[bool, str]:
|
|
280
|
+
path = shutil.which(name)
|
|
281
|
+
return (bool(path), path or f"{name} executable not found")
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class UnavailableNativeAdapter(Adapter):
|
|
285
|
+
"""Explicit native capability gap with a provenance-safe fallback."""
|
|
286
|
+
|
|
287
|
+
name = "native-unavailable"
|
|
288
|
+
|
|
289
|
+
def __init__(self, language: str, expected_parser: str, fallback: Adapter, reason: str):
|
|
290
|
+
self.language = language
|
|
291
|
+
self.expected_parser = expected_parser
|
|
292
|
+
self.fallback = fallback
|
|
293
|
+
self.reason = reason
|
|
294
|
+
|
|
295
|
+
def version(self) -> str:
|
|
296
|
+
return "1"
|
|
297
|
+
|
|
298
|
+
def parser_for_file(self, codebase, file_record) -> str:
|
|
299
|
+
return self.fallback.parser_for_file(codebase, file_record)
|
|
300
|
+
|
|
301
|
+
def parse_file(self, codebase, file_record, text) -> ParseResult:
|
|
302
|
+
result = self.fallback.parse_file(codebase, file_record, text)
|
|
303
|
+
result.actual_parser = result.actual_parser or self.fallback.parser_for_file(codebase, file_record)
|
|
304
|
+
result.diagnostics.append({
|
|
305
|
+
"codebase": codebase.name,
|
|
306
|
+
"path": file_record["path"],
|
|
307
|
+
"line": 1,
|
|
308
|
+
"severity": "warning",
|
|
309
|
+
"kind": "adapter_missing",
|
|
310
|
+
"message": f"native AST unavailable for {self.language}; {self.fallback.parser_id} fallback used: {self.reason}",
|
|
311
|
+
"parser": self.parser_id,
|
|
312
|
+
"metadata": {
|
|
313
|
+
"capability": "native_unavailable",
|
|
314
|
+
"expected_parser": self.expected_parser,
|
|
315
|
+
"fallback_parser": self.fallback.parser_id,
|
|
316
|
+
},
|
|
317
|
+
})
|
|
318
|
+
return result
|
|
319
|
+
|
|
320
|
+
def capability(self) -> Dict:
|
|
321
|
+
return {
|
|
322
|
+
"parser": self.expected_parser,
|
|
323
|
+
"languages": [self.language],
|
|
324
|
+
"native": True,
|
|
325
|
+
"available": False,
|
|
326
|
+
"reason": self.reason,
|
|
327
|
+
"fallback": self.fallback.parser_id,
|
|
328
|
+
}
|