@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.
Files changed (25) hide show
  1. package/package.json +9 -2
  2. package/plugins/devflow-intel/skills/workflow-code-index/references/adapter-contract.md +8 -0
  3. package/plugins/devflow-intel/skills/workflow-code-index/references/data-model.md +1 -1
  4. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/base.py +20 -0
  5. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/capabilities.py +26 -0
  6. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/generic_text.py +4 -2
  7. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/go_ast.py +20 -0
  8. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/helpers/DevflowJavaAstHelper.java +96 -0
  9. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/helpers/go_ast_helper.go +96 -0
  10. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/helpers/php_ast_helper.php +91 -0
  11. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/helpers/tree_sitter_wasm_helper.mjs +50 -0
  12. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/helpers/typescript_ast_helper.cjs +123 -0
  13. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/java_ast.py +37 -0
  14. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/native_process.py +328 -0
  15. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/parser-capabilities.json +491 -0
  16. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/php_ast.py +55 -0
  17. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/python_ast.py +10 -0
  18. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/registry.py +55 -50
  19. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/structured_scope.py +1 -1
  20. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/tree_sitter_wasm.py +253 -0
  21. package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/typescript_ast.py +31 -0
  22. package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +29 -12
  23. package/plugins/devflow-intel/skills/workflow-code-index/scripts/semantic.py +93 -12
  24. package/plugins/devflow-intel/skills/workflow-code-index/scripts/writers/sqlite_writer.py +26 -13
  25. 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
+ }