@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
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
from .capabilities import load_capability_manifest
|
|
1
2
|
from .generic_text import GenericTextAdapter
|
|
3
|
+
from .go_ast import GoAstAdapter
|
|
4
|
+
from .java_ast import JavaAstAdapter
|
|
5
|
+
from .native_process import UnavailableNativeAdapter
|
|
6
|
+
from .php_ast import PhpAstAdapter
|
|
2
7
|
from .python_ast import PythonAstAdapter
|
|
3
8
|
from .structured_scope import StructuredScopeAdapter
|
|
9
|
+
from .tree_sitter_wasm import TreeSitterWasmAdapter
|
|
10
|
+
from .typescript_ast import TypeScriptAstAdapter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
CAPABILITY_MANIFEST = load_capability_manifest()
|
|
4
14
|
|
|
5
15
|
|
|
6
16
|
class AdapterRegistry:
|
|
@@ -8,61 +18,56 @@ class AdapterRegistry:
|
|
|
8
18
|
self.python = PythonAstAdapter()
|
|
9
19
|
self.structured = StructuredScopeAdapter()
|
|
10
20
|
self.generic = GenericTextAdapter()
|
|
11
|
-
self.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
self.typescript = TypeScriptAstAdapter(self.structured)
|
|
22
|
+
self.go = GoAstAdapter(self.structured)
|
|
23
|
+
self.php = PhpAstAdapter(self.structured)
|
|
24
|
+
self.java = JavaAstAdapter(self.structured)
|
|
25
|
+
self.native_adapters = [self.python, self.typescript, self.go, self.php, self.java]
|
|
26
|
+
self.adapters = [*self.native_adapters, self.structured, self.generic]
|
|
17
27
|
self.language_adapters = {
|
|
18
28
|
"python": self.python,
|
|
19
|
-
"typescript": self.
|
|
20
|
-
"javascript": self.
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"php": self.structured,
|
|
25
|
-
"java": self.structured,
|
|
26
|
-
"kotlin": self.structured,
|
|
27
|
-
"c": self.structured,
|
|
28
|
-
"cpp": self.structured,
|
|
29
|
-
"csharp": self.structured,
|
|
30
|
-
"swift": self.structured,
|
|
31
|
-
"dart": self.structured,
|
|
32
|
-
"scala": self.structured,
|
|
33
|
-
"lua": self.structured,
|
|
34
|
-
"shell": self.structured,
|
|
35
|
-
"vue": self.structured,
|
|
36
|
-
"svelte": self.structured,
|
|
37
|
-
# Data, configuration, and template types are explicitly registered too. They
|
|
38
|
-
# currently use the safe text adapter until a native parser is
|
|
39
|
-
# available; this keeps their parser provenance explicit and the
|
|
40
|
-
# unified output contract unchanged.
|
|
41
|
-
"sql": self.generic,
|
|
42
|
-
"html": self.generic,
|
|
43
|
-
"css": self.generic,
|
|
44
|
-
"json": self.generic,
|
|
45
|
-
"yaml": self.generic,
|
|
46
|
-
"toml": self.generic,
|
|
47
|
-
"xml": self.generic,
|
|
48
|
-
"graphql": self.generic,
|
|
49
|
-
"protobuf": self.generic,
|
|
50
|
-
"hcl": self.generic,
|
|
51
|
-
"terraform": self.generic,
|
|
52
|
-
"dockerfile": self.generic,
|
|
53
|
-
"markdown": self.generic,
|
|
29
|
+
"typescript": self.typescript,
|
|
30
|
+
"javascript": self.typescript,
|
|
31
|
+
"go": self.go,
|
|
32
|
+
"php": self.php,
|
|
33
|
+
"java": self.java,
|
|
54
34
|
}
|
|
35
|
+
structured_languages = {
|
|
36
|
+
"ruby", "rust", "kotlin", "c", "cpp", "csharp", "swift",
|
|
37
|
+
"dart", "scala", "lua", "shell", "vue", "svelte",
|
|
38
|
+
}
|
|
39
|
+
for language, declaration in CAPABILITY_MANIFEST["languages"].items():
|
|
40
|
+
if language in self.language_adapters:
|
|
41
|
+
continue
|
|
42
|
+
expected = declaration["parser"]
|
|
43
|
+
reason = declaration.get("todo") or f"{expected} integration is not installed"
|
|
44
|
+
fallback = self.structured if language in structured_languages else self.generic
|
|
45
|
+
if declaration.get("strategy") == "tree-sitter-wasm":
|
|
46
|
+
self.language_adapters[language] = TreeSitterWasmAdapter(language, fallback)
|
|
47
|
+
else:
|
|
48
|
+
self.language_adapters[language] = UnavailableNativeAdapter(language, expected, fallback, reason)
|
|
55
49
|
|
|
56
50
|
def adapter_for_file(self, codebase, file_record):
|
|
57
51
|
language = file_record.get("language")
|
|
58
|
-
return self.language_adapters.get(
|
|
52
|
+
return self.language_adapters.get(
|
|
53
|
+
language,
|
|
54
|
+
UnavailableNativeAdapter(language or "unknown", "unregistered-native-ast", self.generic, "no native adapter is registered"),
|
|
55
|
+
)
|
|
59
56
|
|
|
60
57
|
def adapters_for(self, codebase, files, manifests):
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
58
|
+
return [self.adapter_for_file(codebase, {"language": self._language(path)}) for path in files]
|
|
59
|
+
|
|
60
|
+
def capabilities(self):
|
|
61
|
+
capabilities = {}
|
|
62
|
+
for language, adapter in sorted(self.language_adapters.items()):
|
|
63
|
+
declaration = dict(CAPABILITY_MANIFEST["languages"].get(language, {}))
|
|
64
|
+
runtime = adapter.capability()
|
|
65
|
+
declaration["declaredStatus"] = declaration.pop("status", "unknown")
|
|
66
|
+
declaration.update(runtime)
|
|
67
|
+
capabilities[language] = declaration
|
|
68
|
+
return capabilities
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _language(path):
|
|
72
|
+
from .generic_text import language_for_path
|
|
73
|
+
return language_for_path(path)
|
package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/structured_scope.py
CHANGED
|
@@ -127,7 +127,7 @@ class StructuredScopeAdapter(Adapter):
|
|
|
127
127
|
for target in _extract_calls(line, record["language"]):
|
|
128
128
|
receiver = bool(re.search(rf"\.\s*{re.escape(target)}\s*\(", line))
|
|
129
129
|
relations.append(_relation(owner, "calls", record["path"], number, self.parser_id, "heuristic", to_external=f"call:{target}", metadata={"resolution": "receiver_unresolved" if receiver else "name_only", "allow_unique_name_resolution": not receiver, "dynamic_limitations": True}))
|
|
130
|
-
return ParseResult(symbols=[file_symbol, *symbols], relations=relations, diagnostics=[])
|
|
130
|
+
return ParseResult(symbols=[file_symbol, *symbols], relations=relations, diagnostics=[], actual_parser=self.parser_id)
|
|
131
131
|
|
|
132
132
|
|
|
133
133
|
def _without_strings_and_comments(line: str) -> str:
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Optional Tree-sitter WASM adapter.
|
|
2
|
+
|
|
3
|
+
The indexer remains usable without a grammar bundle. In that case this
|
|
4
|
+
adapter deliberately returns the configured fallback and a diagnostic; it
|
|
5
|
+
never labels lexical facts as Tree-sitter facts. A grammar is supplied by the
|
|
6
|
+
runtime (or DEVFLOW_TREE_SITTER_WASM_GRAMMAR_<LANG>) so the npm package does
|
|
7
|
+
not silently depend on a machine-specific native installation.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import hashlib
|
|
12
|
+
import os
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Dict, Tuple
|
|
17
|
+
|
|
18
|
+
from adapters.base import Adapter, ParseResult
|
|
19
|
+
from adapters.capabilities import load_capability_manifest
|
|
20
|
+
from adapters.generic_text import _relation, _symbol
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
HELPER = Path(__file__).resolve().parent / "helpers" / "tree_sitter_wasm_helper.mjs"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TreeSitterWasmAdapter(Adapter):
|
|
27
|
+
name = "tree-sitter-wasm"
|
|
28
|
+
|
|
29
|
+
def __init__(self, language: str, fallback: Adapter):
|
|
30
|
+
self.language = language
|
|
31
|
+
self.fallback = fallback
|
|
32
|
+
self.declaration = load_capability_manifest()["languages"].get(language, {})
|
|
33
|
+
self._availability: Tuple[bool, str] | None = None
|
|
34
|
+
|
|
35
|
+
def version(self) -> str:
|
|
36
|
+
return str(self.declaration.get("adapter_version", 1))
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def parser_id(self) -> str:
|
|
40
|
+
grammar = self.declaration.get("grammar", {}).get("id", self.language)
|
|
41
|
+
return f"tree-sitter-wasm:{grammar}@{self.version()}"
|
|
42
|
+
|
|
43
|
+
def parser_for_file(self, codebase, file_record) -> str:
|
|
44
|
+
available, _ = self._availability_for(codebase)
|
|
45
|
+
return self.parser_id if available else self.fallback.parser_for_file(codebase, file_record)
|
|
46
|
+
|
|
47
|
+
def check_available(self, codebase=None) -> Tuple[bool, str]:
|
|
48
|
+
grammar = self._grammar_path(codebase)
|
|
49
|
+
if not grammar:
|
|
50
|
+
return False, "Tree-sitter WASM grammar is not configured"
|
|
51
|
+
pin = self._grammar_pin()
|
|
52
|
+
if not pin["version"] or not pin["sha256"]:
|
|
53
|
+
return False, "Tree-sitter WASM grammar is unpinned; configure version and sha256"
|
|
54
|
+
actual_hash = self._sha256(grammar)
|
|
55
|
+
expected_hash = pin["sha256"].removeprefix("sha256:").lower()
|
|
56
|
+
if actual_hash != expected_hash:
|
|
57
|
+
return False, f"Tree-sitter WASM grammar hash mismatch: expected {expected_hash}, got {actual_hash}"
|
|
58
|
+
runtime = self._runtime()
|
|
59
|
+
if runtime is None:
|
|
60
|
+
return False, "Node/Bun runtime is not available"
|
|
61
|
+
if not self._module_available(runtime):
|
|
62
|
+
return False, "web-tree-sitter module is not importable"
|
|
63
|
+
if not HELPER.is_file():
|
|
64
|
+
return False, "Tree-sitter WASM helper is missing"
|
|
65
|
+
return True, f"Tree-sitter WASM grammar available: {grammar} ({pin['version']}, sha256:{actual_hash})"
|
|
66
|
+
|
|
67
|
+
def capability(self) -> Dict:
|
|
68
|
+
available, reason = self._availability_for(None)
|
|
69
|
+
grammar = dict(self.declaration.get("grammar", {}))
|
|
70
|
+
return {
|
|
71
|
+
"parser": self.parser_id,
|
|
72
|
+
"strategy": "tree-sitter-wasm",
|
|
73
|
+
"native": False,
|
|
74
|
+
"available": available,
|
|
75
|
+
"status": "implemented" if available else "parser_unavailable",
|
|
76
|
+
"support_level": "supported" if available else ("unpinned" if self._grammar_path(None) else "parser_unavailable"),
|
|
77
|
+
"pin": self._grammar_pin(),
|
|
78
|
+
"reason": reason,
|
|
79
|
+
"grammar": grammar,
|
|
80
|
+
"fallback": self.fallback.parser_id,
|
|
81
|
+
"scope": "Tree-sitter WASM syntax tree; framework semantics remain a separate linker stage",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def parse_file(self, codebase, file_record, text) -> ParseResult:
|
|
85
|
+
if file_record.get("language") != self.language:
|
|
86
|
+
return self.fallback.parse_file(codebase, file_record, text)
|
|
87
|
+
available, reason = self._availability_for(codebase)
|
|
88
|
+
if not available:
|
|
89
|
+
result = self.fallback.parse_file(codebase, file_record, text)
|
|
90
|
+
result.diagnostics.append({
|
|
91
|
+
"codebase": codebase.name,
|
|
92
|
+
"path": file_record["path"],
|
|
93
|
+
"line": 1,
|
|
94
|
+
"severity": "warning",
|
|
95
|
+
"kind": "parser_unavailable",
|
|
96
|
+
"message": f"Tree-sitter WASM unavailable for {self.language}; fallback used: {reason}",
|
|
97
|
+
"parser": self.parser_id,
|
|
98
|
+
"metadata": {"support_level": "parser_unavailable", "fallback_parser": self.fallback.parser_id},
|
|
99
|
+
})
|
|
100
|
+
return result
|
|
101
|
+
try:
|
|
102
|
+
payload = self._parse(codebase, file_record, text)
|
|
103
|
+
except Exception as error:
|
|
104
|
+
result = self.fallback.parse_file(codebase, file_record, text)
|
|
105
|
+
result.diagnostics.append({
|
|
106
|
+
"codebase": codebase.name, "path": file_record["path"], "line": 1,
|
|
107
|
+
"severity": "error", "kind": "parse_failed",
|
|
108
|
+
"message": f"Tree-sitter WASM parse failed; fallback used: {error}",
|
|
109
|
+
"parser": self.parser_id,
|
|
110
|
+
})
|
|
111
|
+
return result
|
|
112
|
+
return self._convert(codebase, file_record, text, payload)
|
|
113
|
+
|
|
114
|
+
def _availability_for(self, codebase=None):
|
|
115
|
+
# A grammar/runtime is global for a scan. Codebase-specific grammar
|
|
116
|
+
# overrides are intentionally checked only when parsing that codebase.
|
|
117
|
+
if codebase is not None:
|
|
118
|
+
return self.check_available(codebase)
|
|
119
|
+
if self._availability is None:
|
|
120
|
+
self._availability = self.check_available(None)
|
|
121
|
+
return self._availability
|
|
122
|
+
|
|
123
|
+
def _grammar_path(self, codebase=None):
|
|
124
|
+
key = f"DEVFLOW_TREE_SITTER_WASM_GRAMMAR_{self.language.upper().replace('-', '_')}"
|
|
125
|
+
configured = os.environ.get(key) or self.declaration.get("grammar", {}).get("path")
|
|
126
|
+
if configured:
|
|
127
|
+
path = Path(configured)
|
|
128
|
+
if not path.is_absolute() and codebase is not None:
|
|
129
|
+
path = Path(codebase.root) / path
|
|
130
|
+
return str(path.resolve()) if path.is_file() else None
|
|
131
|
+
package = self.declaration.get("grammar", {}).get("package")
|
|
132
|
+
package_path = self._resolve_package_grammar(package) if package else None
|
|
133
|
+
return package_path
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _resolve_package_grammar(package):
|
|
137
|
+
"""Resolve a declared npm grammar without downloading at runtime.
|
|
138
|
+
|
|
139
|
+
The package is an installation-time dependency. Searching the
|
|
140
|
+
workspace and adapter ancestors keeps this working from both a
|
|
141
|
+
checkout and an installed plugin while ensuring no network fallback
|
|
142
|
+
can occur during indexing.
|
|
143
|
+
"""
|
|
144
|
+
if not package:
|
|
145
|
+
return None
|
|
146
|
+
roots = [Path.cwd(), Path(__file__).resolve()]
|
|
147
|
+
seen = set()
|
|
148
|
+
for root in roots:
|
|
149
|
+
for candidate in [root, *root.parents]:
|
|
150
|
+
package_root = candidate / "node_modules" / package
|
|
151
|
+
candidates = [package_root / "grammar.wasm"]
|
|
152
|
+
if package_root.is_dir():
|
|
153
|
+
candidates.extend(sorted(package_root.glob("*.wasm")))
|
|
154
|
+
for node_modules in candidates:
|
|
155
|
+
key = str(node_modules)
|
|
156
|
+
if key in seen:
|
|
157
|
+
continue
|
|
158
|
+
seen.add(key)
|
|
159
|
+
if node_modules.is_file():
|
|
160
|
+
return str(node_modules.resolve())
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
def _grammar_pin(self):
|
|
164
|
+
grammar = self.declaration.get("grammar", {})
|
|
165
|
+
prefix = f"DEVFLOW_TREE_SITTER_GRAMMAR_{self.language.upper().replace('-', '_')}"
|
|
166
|
+
return {
|
|
167
|
+
"version": os.environ.get(f"{prefix}_VERSION") or grammar.get("version"),
|
|
168
|
+
"sha256": os.environ.get(f"{prefix}_SHA256") or grammar.get("sha256"),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _sha256(path):
|
|
173
|
+
digest = hashlib.sha256()
|
|
174
|
+
with Path(path).open("rb") as handle:
|
|
175
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
176
|
+
digest.update(chunk)
|
|
177
|
+
return digest.hexdigest()
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def _module_available(runtime):
|
|
181
|
+
# Probe the module before claiming availability. The helper import is
|
|
182
|
+
# deliberately not the first place this is discovered: otherwise the
|
|
183
|
+
# snapshot would advertise a parser which only fails per file.
|
|
184
|
+
try:
|
|
185
|
+
probe = subprocess.run(
|
|
186
|
+
[runtime, "--input-type=module", "-e",
|
|
187
|
+
"import('web-tree-sitter').then(()=>process.exit(0)).catch(()=>process.exit(1))"],
|
|
188
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
189
|
+
timeout=5, check=False,
|
|
190
|
+
)
|
|
191
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
192
|
+
return False
|
|
193
|
+
return probe.returncode == 0
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def _runtime():
|
|
197
|
+
configured = os.environ.get("DEVFLOW_TREE_SITTER_WASM_RUNTIME")
|
|
198
|
+
if configured:
|
|
199
|
+
return configured if shutil.which(configured) else None
|
|
200
|
+
return shutil.which("bun") or shutil.which("node")
|
|
201
|
+
|
|
202
|
+
def _parse(self, codebase, record, text):
|
|
203
|
+
runtime = self._runtime()
|
|
204
|
+
grammar = self._grammar_path(codebase)
|
|
205
|
+
max_bytes = int(os.environ.get("DEVFLOW_TREE_SITTER_WASM_MAX_BYTES", str(2 * 1024 * 1024)))
|
|
206
|
+
source_bytes = len(text.encode("utf-8"))
|
|
207
|
+
if source_bytes > max_bytes:
|
|
208
|
+
raise RuntimeError(f"source exceeds Tree-sitter WASM limit ({source_bytes} > {max_bytes} bytes)")
|
|
209
|
+
timeout = float(os.environ.get("DEVFLOW_TREE_SITTER_WASM_TIMEOUT", "10"))
|
|
210
|
+
proc = subprocess.run(
|
|
211
|
+
[runtime, str(HELPER), grammar],
|
|
212
|
+
input=json.dumps({"path": record["path"], "source": text}, ensure_ascii=False) + "\n",
|
|
213
|
+
text=True, encoding="utf-8", stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
214
|
+
timeout=timeout, check=False,
|
|
215
|
+
)
|
|
216
|
+
if proc.returncode != 0:
|
|
217
|
+
raise RuntimeError(proc.stderr.strip()[:500] or f"helper exited {proc.returncode}")
|
|
218
|
+
payload = json.loads(proc.stdout.splitlines()[-1])
|
|
219
|
+
if not payload.get("ok"):
|
|
220
|
+
raise RuntimeError(payload.get("error") or "Tree-sitter helper returned no result")
|
|
221
|
+
return payload
|
|
222
|
+
|
|
223
|
+
def _convert(self, codebase, record, text, payload):
|
|
224
|
+
path, content_hash = record["path"], record["content_hash"]
|
|
225
|
+
file_symbol = _symbol(codebase.name, record["language"], "file", Path(path).name, path, 1,
|
|
226
|
+
max(1, len(text.splitlines())), content_hash, qualified_name=path,
|
|
227
|
+
parser=self.parser_id)
|
|
228
|
+
symbols = [file_symbol]
|
|
229
|
+
by_key = {"": file_symbol}
|
|
230
|
+
for item in payload.get("declarations", []):
|
|
231
|
+
parent = by_key.get(str(item.get("parent") or ""), file_symbol)
|
|
232
|
+
symbol = _symbol(codebase.name, record["language"], item.get("kind", "symbol"),
|
|
233
|
+
item.get("name", "anonymous"), path, max(1, int(item.get("line_start", 1))),
|
|
234
|
+
max(1, int(item.get("line_end", item.get("line_start", 1)))), content_hash,
|
|
235
|
+
qualified_name=item.get("qualified_name") or f"{parent['qualified_name']}::{item.get('name', 'anonymous')}",
|
|
236
|
+
parser=self.parser_id)
|
|
237
|
+
symbol["container_id"] = parent["id"]
|
|
238
|
+
symbol["metadata"].update({"parse_status": payload.get("parse_status", "complete"), "grammar": self.declaration.get("grammar", {})})
|
|
239
|
+
symbols.append(symbol)
|
|
240
|
+
by_key[str(item.get("key"))] = symbol
|
|
241
|
+
relations = []
|
|
242
|
+
for symbol in symbols[1:]:
|
|
243
|
+
relations.append(_relation(symbol["container_id"], "contains", path, symbol["line_start"], self.parser_id, "exact", to_id=symbol["id"]))
|
|
244
|
+
for item in payload.get("calls", []):
|
|
245
|
+
owner = by_key.get(str(item.get("owner") or ""), file_symbol)
|
|
246
|
+
relations.append(_relation(owner["id"], "calls", path, max(1, int(item.get("line", 1))), self.parser_id,
|
|
247
|
+
"heuristic", to_external=f"call:{item.get('target', 'unknown')}"))
|
|
248
|
+
diagnostics = list(payload.get("diagnostics", []))
|
|
249
|
+
if payload.get("parse_status") != "complete":
|
|
250
|
+
diagnostics.append({"codebase": codebase.name, "path": path, "line": 1, "severity": "warning",
|
|
251
|
+
"kind": "parse_partial", "message": "Tree-sitter grammar produced a partial tree",
|
|
252
|
+
"parser": self.parser_id, "metadata": payload.get("metadata", {})})
|
|
253
|
+
return ParseResult(symbols=symbols, relations=relations, diagnostics=diagnostics, actual_parser=self.parser_id)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Tuple
|
|
5
|
+
|
|
6
|
+
from adapters.native_process import HELPER_DIR, NativeProcessAdapter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TypeScriptAstAdapter(NativeProcessAdapter):
|
|
10
|
+
name = "typescript-compiler-ast"
|
|
11
|
+
languages = ("typescript", "javascript")
|
|
12
|
+
native_description = "TypeScript Compiler API syntax tree; declarations, imports, and calls"
|
|
13
|
+
|
|
14
|
+
def version(self) -> str:
|
|
15
|
+
return "1"
|
|
16
|
+
|
|
17
|
+
def check_available(self, codebase=None) -> Tuple[bool, str]:
|
|
18
|
+
node = shutil.which("node")
|
|
19
|
+
if not node:
|
|
20
|
+
return False, "node executable not found"
|
|
21
|
+
helper = HELPER_DIR / "typescript_ast_helper.cjs"
|
|
22
|
+
probe = subprocess.run(
|
|
23
|
+
[node, "-e", "require.resolve('typescript')"], cwd=str(helper.parent),
|
|
24
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False,
|
|
25
|
+
)
|
|
26
|
+
if probe.returncode != 0:
|
|
27
|
+
return False, "typescript compiler API module not found"
|
|
28
|
+
return True, "typescript compiler API available"
|
|
29
|
+
|
|
30
|
+
def command(self, codebase=None) -> List[str]:
|
|
31
|
+
return [shutil.which("node") or "node", str(HELPER_DIR / "typescript_ast_helper.cjs")]
|
|
@@ -22,6 +22,7 @@ if str(SCRIPT_DIR) not in sys.path:
|
|
|
22
22
|
sys.path.insert(0, str(SCRIPT_DIR))
|
|
23
23
|
|
|
24
24
|
from adapters.generic_text import language_for_path
|
|
25
|
+
from adapters.capabilities import confidence_for_parser
|
|
25
26
|
from adapters.registry import AdapterRegistry
|
|
26
27
|
from semantic import SEMANTIC_EXTRACTOR_VERSION, build_semantic_facts
|
|
27
28
|
from core.discovery import discover_codebases
|
|
@@ -99,7 +100,7 @@ def _publish_snapshot(staging: Path, output: Path, counts: Dict[str, int], works
|
|
|
99
100
|
os.close(directory_fd)
|
|
100
101
|
|
|
101
102
|
|
|
102
|
-
def _workspace_state(root: Path, files: List[dict], changed: Set[str], discovered_paths: Iterable[str] | None = None) -> Dict[str, object]:
|
|
103
|
+
def _workspace_state(root: Path, files: List[dict], changed: Set[str], discovered_paths: Iterable[str] | None = None, capabilities: Dict[str, object] | None = None) -> Dict[str, object]:
|
|
103
104
|
indexed_paths = sorted(record["path"] for record in files)
|
|
104
105
|
discovered = sorted(set(discovered_paths or indexed_paths))
|
|
105
106
|
source_manifest_hash = _hash_lines(f"{record['path']}\0{record['content_hash']}" for record in files)
|
|
@@ -112,11 +113,7 @@ def _workspace_state(root: Path, files: List[dict], changed: Set[str], discovere
|
|
|
112
113
|
"dirtyPathsAtBuild": sorted(changed),
|
|
113
114
|
"extractors": extractors,
|
|
114
115
|
"extractorSetHash": _hash_lines(extractors),
|
|
115
|
-
"capabilities": {
|
|
116
|
-
"python-ast@1": {"scope": "exact syntax tree", "targets": "lexical names; dynamic dispatch unresolved"},
|
|
117
|
-
"structured-scope@1": {"scope": "inferred lexical braces/end blocks", "targets": "name-only heuristic"},
|
|
118
|
-
"generic-text@2": {"scope": "file-level", "targets": "heuristic only"},
|
|
119
|
-
},
|
|
116
|
+
"capabilities": capabilities or {},
|
|
120
117
|
}
|
|
121
118
|
|
|
122
119
|
|
|
@@ -290,14 +287,27 @@ def command_scan(args) -> int:
|
|
|
290
287
|
"parser": "pending",
|
|
291
288
|
}
|
|
292
289
|
adapter = registry.adapter_for_file(codebase, file_record)
|
|
293
|
-
|
|
290
|
+
parser_for_file = getattr(adapter, "parser_for_file", None)
|
|
291
|
+
file_record["parser"] = parser_for_file(codebase, file_record) if parser_for_file else adapter.parser_id
|
|
294
292
|
result = adapter.parse_file(codebase, file_record, text)
|
|
293
|
+
file_record["parser"] = result.actual_parser or file_record["parser"]
|
|
295
294
|
files.append(file_record)
|
|
296
295
|
symbols.extend(result.symbols)
|
|
297
296
|
relations.extend(result.relations)
|
|
298
297
|
diagnostics.extend(result.diagnostics)
|
|
299
|
-
|
|
300
|
-
|
|
298
|
+
for entity in getattr(result, "entities", []) or []:
|
|
299
|
+
entity = dict(entity)
|
|
300
|
+
entity.setdefault("extractor", file_record["parser"])
|
|
301
|
+
entity.setdefault("confidence", confidence_for_parser(file_record["parser"], "symbol"))
|
|
302
|
+
entity.setdefault("metadata", {})
|
|
303
|
+
adapter_entities.append(entity)
|
|
304
|
+
for flow in getattr(result, "flows", []) or []:
|
|
305
|
+
flow = dict(flow)
|
|
306
|
+
flow.setdefault("extractor", file_record["parser"])
|
|
307
|
+
flow.setdefault("confidence", "heuristic")
|
|
308
|
+
flow.setdefault("evidence_level", "syntax")
|
|
309
|
+
flow.setdefault("metadata", {})
|
|
310
|
+
adapter_flows.append(flow)
|
|
301
311
|
parsed += 1
|
|
302
312
|
if parsed == 1 or parsed % 250 == 0 or parsed == len(all_files):
|
|
303
313
|
progress.emit("parse", f"parsed {parsed}/{len(all_files)} files in {codebase.name}", codebase=codebase.name, parsed=parsed, total=len(all_files))
|
|
@@ -319,12 +329,19 @@ def command_scan(args) -> int:
|
|
|
319
329
|
staging = Path(tempfile.mkdtemp(prefix=f".{output.name}.build-", dir=str(output.parent)))
|
|
320
330
|
try:
|
|
321
331
|
generation = str(uuid.uuid4())
|
|
322
|
-
|
|
332
|
+
registry_capabilities = getattr(registry, "capabilities", None)
|
|
333
|
+
workspace_state = _workspace_state(
|
|
334
|
+
root, files, changed, discovered_paths,
|
|
335
|
+
registry_capabilities() if registry_capabilities else {},
|
|
336
|
+
)
|
|
323
337
|
workspace_state["generatedAt"] = datetime.now(timezone.utc).isoformat()
|
|
324
338
|
workspace_state["semanticExtractor"] = SEMANTIC_EXTRACTOR_VERSION
|
|
325
339
|
workspace_state["extractorSetHash"] = _hash_lines([*workspace_state.get("extractors", []), SEMANTIC_EXTRACTOR_VERSION])
|
|
326
340
|
progress.emit("semantic", "building semantic facts")
|
|
327
|
-
|
|
341
|
+
semantic_stats = {}
|
|
342
|
+
semantic_entities, semantic_flows, feature_bindings = build_semantic_facts(root, files, symbols, relations, stats=semantic_stats)
|
|
343
|
+
workspace_state["semanticScan"] = semantic_stats
|
|
344
|
+
progress.emit("semantic", "semantic facts completed", **semantic_stats)
|
|
328
345
|
semantic_entities.extend(adapter_entities)
|
|
329
346
|
semantic_flows.extend(adapter_flows)
|
|
330
347
|
write_jsonl(staging / INDEX_FILES["files"], files)
|
|
@@ -484,7 +501,7 @@ def command_relations(args) -> int:
|
|
|
484
501
|
truncated = len(rows) > limit
|
|
485
502
|
for row in rows[:limit]:
|
|
486
503
|
item = dict(row)
|
|
487
|
-
if args
|
|
504
|
+
if getattr(args, "change_type", None):
|
|
488
505
|
item["impact_kind"] = args.change_type
|
|
489
506
|
item["severity"] = _impact_severity(args.change_type, item["flow_type"])
|
|
490
507
|
print(json.dumps(item, ensure_ascii=False, sort_keys=True))
|