@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.76",
3
+ "version": "0.1.78",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -20,7 +20,14 @@
20
20
  "bun": ">=1.3.14"
21
21
  },
22
22
  "dependencies": {
23
- "@oh-my-pi/pi-coding-agent": "17.2.7"
23
+ "@oh-my-pi/pi-coding-agent": "17.2.7",
24
+ "@plurnk/plurnk-mimetypes-grammar-cpp": "1.16.1",
25
+ "@plurnk/plurnk-mimetypes-grammar-kotlin": "1.16.1",
26
+ "@plurnk/plurnk-mimetypes-grammar-ruby": "1.16.1",
27
+ "@plurnk/plurnk-mimetypes-grammar-scala": "1.16.1",
28
+ "@plurnk/plurnk-mimetypes-grammar-yaml": "1.16.1",
29
+ "tree-sitter-graphql-grammar-wasm": "1.1.0",
30
+ "web-tree-sitter": "0.25.10"
24
31
  },
25
32
  "devDependencies": {
26
33
  "@types/bun": "^1.3.14",
@@ -18,6 +18,9 @@ The result contains:
18
18
  - `symbols`
19
19
  - `relations`
20
20
  - `diagnostics`
21
+ - `actual_parser` — parser that produced the returned facts; required when a
22
+ selected native adapter falls back after a dependency or per-file parse
23
+ failure
21
24
 
22
25
  ## Resolution Rules
23
26
 
@@ -36,3 +39,8 @@ The result contains:
36
39
 
37
40
  Core schema and writers must not change when a new adapter is added.
38
41
 
42
+ Declared and runtime parser capabilities are recorded in
43
+ `scripts/adapters/parser-capabilities.json` and the snapshot capability map.
44
+ An unavailable native or Tree-sitter parser must report its fallback and emit
45
+ an `adapter_missing` diagnostic; structured lexical output must never be
46
+ labelled as native AST output.
@@ -34,6 +34,7 @@ All adapters must emit these records. Examples use placeholders only; do not rep
34
34
  "language": "{language_id}",
35
35
  "content_hash": "sha256:{hash}",
36
36
  "visibility": "{optional_visibility}",
37
+ "parser": "{adapter}@{version}",
37
38
  "metadata": {}
38
39
  }
39
40
  ```
@@ -71,4 +72,3 @@ Symbol IDs must not depend on line numbers. Use language, codebase, stable quali
71
72
  "parser": "{adapter}@{version}"
72
73
  }
73
74
  ```
74
-
@@ -12,6 +12,9 @@ class ParseResult:
12
12
  # in a separate linker pass.
13
13
  entities: List[Dict] = field(default_factory=list)
14
14
  flows: List[Dict] = field(default_factory=list)
15
+ # Parser that actually produced this result. This can differ from the
16
+ # selected adapter when a native parser fails and a fallback is used.
17
+ actual_parser: str = ""
15
18
 
16
19
 
17
20
  class Adapter:
@@ -29,6 +32,23 @@ class Adapter:
29
32
  def version(self) -> str:
30
33
  return "0"
31
34
 
35
+ def parser_for_file(self, codebase, file_record) -> str:
36
+ """Return the parser that will actually produce this file's facts.
37
+
38
+ Native adapters override this when their runtime dependency is not
39
+ available and a declared fallback will be used instead. Keeping this
40
+ separate from ``parser_id`` prevents fallback output from being
41
+ labelled as native AST output.
42
+ """
43
+ return self.parser_id
44
+
45
+ def capability(self) -> Dict:
46
+ return {
47
+ "parser": self.parser_id,
48
+ "native": False,
49
+ "available": True,
50
+ }
51
+
32
52
  @property
33
53
  def parser_id(self) -> str:
34
54
  return f"{self.name}@{self.version()}"
@@ -0,0 +1,26 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Dict
4
+
5
+
6
+ MANIFEST_PATH = Path(__file__).resolve().parent / "parser-capabilities.json"
7
+
8
+
9
+ def load_capability_manifest() -> Dict:
10
+ manifest = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
11
+ if manifest.get("schemaVersion") != 1 or not isinstance(manifest.get("languages"), dict):
12
+ raise ValueError("invalid parser capability manifest")
13
+ return manifest
14
+
15
+
16
+ def confidence_for_parser(parser: str, kind: str = "symbol") -> str:
17
+ """Map extractor provenance to the strongest defensible confidence."""
18
+ if kind == "file":
19
+ return "exact"
20
+ if parser.startswith(("python-ast@", "typescript-compiler-ast@", "go-parser-ast@", "javac-tree-ast@", "nikic-php-parser-ast@")):
21
+ return "exact"
22
+ if parser.startswith("tree-sitter-"):
23
+ return "exact"
24
+ if parser.startswith("structured-scope@"):
25
+ return "inferred"
26
+ return "heuristic"
@@ -95,8 +95,9 @@ class GenericTextAdapter(Adapter):
95
95
  path,
96
96
  symbol["line_start"],
97
97
  self.parser_id,
98
- confidence="exact",
98
+ confidence="heuristic",
99
99
  to_id=symbol["id"],
100
+ metadata={"extraction": "text_pattern"},
100
101
  )
101
102
  )
102
103
 
@@ -155,7 +156,7 @@ class GenericTextAdapter(Adapter):
155
156
  }
156
157
  )
157
158
 
158
- return ParseResult(symbols=symbols, relations=relations, diagnostics=diagnostics)
159
+ return ParseResult(symbols=symbols, relations=relations, diagnostics=diagnostics, actual_parser=self.parser_id)
159
160
 
160
161
  def _extract_symbols(self, codebase: str, language: str, path: str, text: str, content_hash: str) -> List[Dict]:
161
162
  symbols: List[Dict] = []
@@ -289,6 +290,7 @@ def _symbol(codebase, language, kind, name, path, line_start, line_end, content_
289
290
  "language": language,
290
291
  "content_hash": content_hash,
291
292
  "visibility": visibility,
293
+ "parser": parser,
292
294
  "metadata": {"parser": parser} if parser else {},
293
295
  }
294
296
 
@@ -0,0 +1,20 @@
1
+ import shutil
2
+ from typing import List, Tuple
3
+
4
+ from adapters.native_process import HELPER_DIR, NativeProcessAdapter
5
+
6
+
7
+ class GoAstAdapter(NativeProcessAdapter):
8
+ name = "go-parser-ast"
9
+ languages = ("go",)
10
+ native_description = "Go standard library go parser AST; declarations, imports, and calls"
11
+
12
+ def version(self) -> str:
13
+ return "1"
14
+
15
+ def check_available(self, codebase=None) -> Tuple[bool, str]:
16
+ go = shutil.which("go")
17
+ return (True, "go/parser available") if go else (False, "go executable not found")
18
+
19
+ def command(self, codebase=None) -> List[str]:
20
+ return [shutil.which("go") or "go", "run", str(HELPER_DIR / "go_ast_helper.go")]
@@ -0,0 +1,96 @@
1
+ import java.io.*;
2
+ import java.net.URI;
3
+ import java.nio.charset.StandardCharsets;
4
+ import java.util.*;
5
+ import javax.tools.*;
6
+ import javax.lang.model.element.Modifier;
7
+ import com.sun.source.tree.*;
8
+ import com.sun.source.util.*;
9
+
10
+ public final class DevflowJavaAstHelper {
11
+ private static String json(String value) {
12
+ if (value == null) return "\"\"";
13
+ StringBuilder out = new StringBuilder("\"");
14
+ for (char c : value.toCharArray()) {
15
+ switch (c) {
16
+ case '\\': out.append("\\\\"); break;
17
+ case '"': out.append("\\\""); break;
18
+ case '\n': out.append("\\n"); break;
19
+ case '\r': out.append("\\r"); break;
20
+ case '\t': out.append("\\t"); break;
21
+ default:
22
+ if (c < 32) out.append(String.format("\\u%04x", (int)c)); else out.append(c);
23
+ }
24
+ }
25
+ return out.append('"').toString();
26
+ }
27
+
28
+ private static final class Source extends SimpleJavaFileObject {
29
+ final String text;
30
+ Source(String path, String text) { super(URI.create("string:///" + path.replace(' ', '_')), Kind.SOURCE); this.text = text; }
31
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) { return text; }
32
+ }
33
+
34
+ private static final class Decl {
35
+ String key, parent, kind, name, signature, nativeKind, visibility; int start, end;
36
+ String toJson() { return "{\"key\":"+json(key)+",\"parent\":"+json(parent)+",\"kind\":"+json(kind)+",\"name\":"+json(name)+",\"line_start\":"+start+",\"line_end\":"+end+",\"signature\":"+json(signature)+",\"native_kind\":"+json(nativeKind)+",\"visibility\":"+json(visibility)+"}"; }
37
+ }
38
+ private static final class Rel {
39
+ String owner, type, target, confidence, resolution; int line;
40
+ String toJson() { return "{\"owner\":"+json(owner)+",\"type\":"+json(type)+",\"target\":"+json(target)+",\"line\":"+line+",\"confidence\":"+json(confidence)+",\"metadata\":{\"resolution\":"+json(resolution)+"}}"; }
41
+ }
42
+
43
+ private static String parse(String path, String sourceText) throws Exception {
44
+ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
45
+ if (compiler == null) throw new IllegalStateException("JDK compiler API unavailable");
46
+ DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
47
+ JavacTask task = (JavacTask) compiler.getTask(null, null, diagnostics, List.of("-proc:none"), null, List.of(new Source(path, sourceText)));
48
+ CompilationUnitTree unit = task.parse().iterator().next();
49
+ Trees trees = Trees.instance(task); SourcePositions positions = trees.getSourcePositions(); LineMap lines = unit.getLineMap();
50
+ List<Decl> declarations = new ArrayList<>(); List<Rel> relations = new ArrayList<>();
51
+ new TreePathScanner<Void, String>() {
52
+ int next = 1;
53
+ int line(Tree tree) { long p = positions.getStartPosition(unit, tree); return p < 0 ? 1 : (int)lines.getLineNumber(p); }
54
+ int end(Tree tree) { long p = positions.getEndPosition(unit, tree); return p < 0 ? line(tree) : (int)lines.getLineNumber(Math.max(0, p - 1)); }
55
+ String visibility(Set<Modifier> modifiers) { if (modifiers.contains(Modifier.PUBLIC)) return "public"; if (modifiers.contains(Modifier.PROTECTED)) return "protected"; if (modifiers.contains(Modifier.PRIVATE)) return "private"; return ""; }
56
+ String addDecl(Tree tree, String parent, String kind, String name, String signature, Set<Modifier> modifiers) {
57
+ Decl d = new Decl(); d.key = String.valueOf(next++); d.parent = parent == null ? "" : parent; d.kind = kind; d.name = name; d.signature = signature; d.nativeKind = tree.getKind().name(); d.visibility = visibility(modifiers); d.start = line(tree); d.end = Math.max(d.start, end(tree)); declarations.add(d); return d.key;
58
+ }
59
+ void addRel(String owner, String type, String target, Tree tree, String confidence, String resolution) { Rel r = new Rel(); r.owner=owner==null?"":owner; r.type=type; r.target=target; r.line=line(tree); r.confidence=confidence; r.resolution=resolution; relations.add(r); }
60
+ @Override public Void visitClass(ClassTree node, String owner) {
61
+ String kind = node.getKind() == Tree.Kind.INTERFACE || node.getKind() == Tree.Kind.ANNOTATION_TYPE ? "interface" : "class";
62
+ String key = addDecl(node, owner, kind, node.getSimpleName().toString(), node.getSimpleName()+"()", node.getModifiers().getFlags());
63
+ if (node.getExtendsClause() != null) addRel(key, "extends", "type:"+node.getExtendsClause(), node.getExtendsClause(), "inferred", "syntax_name");
64
+ for (Tree inherited : node.getImplementsClause()) addRel(key, "implements", "type:"+inherited, inherited, "inferred", "syntax_name");
65
+ return super.visitClass(node, key);
66
+ }
67
+ @Override public Void visitMethod(MethodTree node, String owner) {
68
+ String name = node.getName().toString(); String kind = name.toLowerCase().startsWith("test") ? "test" : "method";
69
+ String key = addDecl(node, owner, kind, name, name+"("+String.join(", ", node.getParameters().stream().map(v -> v.getName().toString()).toList())+")", node.getModifiers().getFlags());
70
+ return super.visitMethod(node, key);
71
+ }
72
+ @Override public Void visitImport(ImportTree node, String owner) { addRel(owner, "imports", "import:"+node.getQualifiedIdentifier(), node, "exact", "import_tree"); return super.visitImport(node, owner); }
73
+ @Override public Void visitMethodInvocation(MethodInvocationTree node, String owner) {
74
+ String target=node.getMethodSelect().toString(); boolean receiver=target.contains("."); addRel(owner, "calls", "call:"+target, node, receiver?"heuristic":"inferred", receiver?"receiver_unresolved":"lexical_name"); return super.visitMethodInvocation(node, owner);
75
+ }
76
+ @Override public Void visitNewClass(NewClassTree node, String owner) { addRel(owner, "calls", "call:"+node.getIdentifier(), node, "inferred", "constructor_syntax"); return super.visitNewClass(node, owner); }
77
+ }.scan(unit, "");
78
+ StringBuilder out = new StringBuilder("{\"ok\":true,\"declarations\":[");
79
+ for (int i=0;i<declarations.size();i++) { if(i>0) out.append(','); out.append(declarations.get(i).toJson()); }
80
+ out.append("],\"relations\":["); for(int i=0;i<relations.size();i++){if(i>0)out.append(',');out.append(relations.get(i).toJson());}
81
+ out.append("],\"diagnostics\":["); boolean first=true; for(Diagnostic<? extends JavaFileObject> d: diagnostics.getDiagnostics()){if(!first)out.append(',');first=false;out.append("{\"line\":").append(Math.max(1,d.getLineNumber())).append(",\"severity\":\"warning\",\"kind\":\"parse_failed\",\"message\":").append(json(d.getMessage(Locale.ROOT))).append('}');}
82
+ return out.append("]}").toString();
83
+ }
84
+
85
+ public static void main(String[] args) throws Exception {
86
+ BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)); String line;
87
+ while ((line = reader.readLine()) != null) {
88
+ try {
89
+ String[] parts=line.split("\\t",2); if(parts.length!=2)throw new IllegalArgumentException("invalid request");
90
+ String path=new String(Base64.getDecoder().decode(parts[0]),StandardCharsets.UTF_8); String source=new String(Base64.getDecoder().decode(parts[1]),StandardCharsets.UTF_8);
91
+ System.out.println(parse(path,source));
92
+ } catch(Exception error) { System.out.println("{\"ok\":false,\"error\":"+json(error.toString())+"}"); }
93
+ System.out.flush();
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,96 @@
1
+ package main
2
+
3
+ import (
4
+ "bufio"
5
+ "encoding/json"
6
+ "fmt"
7
+ "go/ast"
8
+ "go/parser"
9
+ "go/token"
10
+ "os"
11
+ "strings"
12
+ )
13
+
14
+ type Request struct { Path string `json:"path"`; Source string `json:"source"` }
15
+ type Declaration struct { Key string `json:"key"`; Parent string `json:"parent"`; Kind string `json:"kind"`; Name string `json:"name"`; LineStart int `json:"line_start"`; LineEnd int `json:"line_end"`; Signature string `json:"signature"`; NativeKind string `json:"native_kind"` }
16
+ type Relation struct { Owner string `json:"owner"`; Type string `json:"type"`; Target string `json:"target"`; Line int `json:"line"`; Confidence string `json:"confidence"`; Metadata map[string]any `json:"metadata"` }
17
+ type Diagnostic struct { Line int `json:"line"`; Severity string `json:"severity"`; Kind string `json:"kind"`; Message string `json:"message"` }
18
+ type Response struct { Ok bool `json:"ok"`; Error string `json:"error,omitempty"`; Declarations []Declaration `json:"declarations,omitempty"`; Relations []Relation `json:"relations,omitempty"`; Diagnostics []Diagnostic `json:"diagnostics,omitempty"` }
19
+
20
+ func expressionName(expr ast.Expr) string {
21
+ switch value := expr.(type) {
22
+ case *ast.Ident: return value.Name
23
+ case *ast.SelectorExpr:
24
+ parent := expressionName(value.X)
25
+ if parent != "" { return parent + "." + value.Sel.Name }
26
+ return value.Sel.Name
27
+ case *ast.IndexExpr: return expressionName(value.X)
28
+ case *ast.IndexListExpr: return expressionName(value.X)
29
+ }
30
+ return ""
31
+ }
32
+
33
+ func parseRequest(request Request) Response {
34
+ fset := token.NewFileSet()
35
+ file, err := parser.ParseFile(fset, request.Path, request.Source, parser.AllErrors|parser.ParseComments)
36
+ response := Response{Ok: true, Declarations: []Declaration{}, Relations: []Relation{}, Diagnostics: []Diagnostic{}}
37
+ if err != nil {
38
+ line := 1
39
+ if list, ok := err.(interface{ Error() string }); ok { _ = list }
40
+ response.Diagnostics = append(response.Diagnostics, Diagnostic{Line: line, Severity: "warning", Kind: "parse_failed", Message: err.Error()})
41
+ }
42
+ if file == nil { response.Ok = false; response.Error = "go/parser produced no syntax tree"; return response }
43
+ keys := map[ast.Node]string{}
44
+ next := 1
45
+ for _, declaration := range file.Decls {
46
+ switch node := declaration.(type) {
47
+ case *ast.FuncDecl:
48
+ key := fmt.Sprintf("%d", next); next++
49
+ keys[node] = key
50
+ kind := "function"
51
+ if node.Recv != nil { kind = "method" }
52
+ if strings.HasPrefix(strings.ToLower(node.Name.Name), "test") { kind = "test" }
53
+ response.Declarations = append(response.Declarations, Declaration{Key:key, Kind:kind, Name:node.Name.Name, LineStart:fset.Position(node.Pos()).Line, LineEnd:fset.Position(node.End()).Line, Signature:node.Name.Name+"()", NativeKind:"FuncDecl"})
54
+ case *ast.GenDecl:
55
+ for _, spec := range node.Specs {
56
+ if typeSpec, ok := spec.(*ast.TypeSpec); ok {
57
+ key := fmt.Sprintf("%d", next); next++
58
+ keys[typeSpec] = key
59
+ kind := "class"
60
+ native := "TypeSpec"
61
+ switch typeSpec.Type.(type) { case *ast.StructType: kind="struct"; native="StructType"; case *ast.InterfaceType: kind="interface"; native="InterfaceType" }
62
+ response.Declarations = append(response.Declarations, Declaration{Key:key, Kind:kind, Name:typeSpec.Name.Name, LineStart:fset.Position(typeSpec.Pos()).Line, LineEnd:fset.Position(typeSpec.End()).Line, Signature:typeSpec.Name.Name, NativeKind:native})
63
+ }
64
+ }
65
+ }
66
+ }
67
+ for _, imp := range file.Imports {
68
+ target := strings.Trim(imp.Path.Value, "\"")
69
+ response.Relations = append(response.Relations, Relation{Type:"imports", Target:"import:"+target, Line:fset.Position(imp.Pos()).Line, Confidence:"exact", Metadata:map[string]any{"resolution":"import_spec"}})
70
+ }
71
+ for _, declaration := range file.Decls {
72
+ owner := keys[declaration]
73
+ ast.Inspect(declaration, func(node ast.Node) bool {
74
+ call, ok := node.(*ast.CallExpr)
75
+ if !ok { return true }
76
+ name := expressionName(call.Fun)
77
+ if name == "" { return true }
78
+ confidence := "inferred"; resolution := "lexical_name"
79
+ if strings.Contains(name, ".") { confidence = "heuristic"; resolution = "receiver_unresolved" }
80
+ response.Relations = append(response.Relations, Relation{Owner:owner, Type:"calls", Target:"call:"+name, Line:fset.Position(call.Pos()).Line, Confidence:confidence, Metadata:map[string]any{"resolution":resolution}})
81
+ return true
82
+ })
83
+ }
84
+ return response
85
+ }
86
+
87
+ func main() {
88
+ scanner := bufio.NewScanner(os.Stdin)
89
+ buffer := make([]byte, 64*1024); scanner.Buffer(buffer, 64*1024*1024)
90
+ encoder := json.NewEncoder(os.Stdout)
91
+ for scanner.Scan() {
92
+ var request Request
93
+ if err := json.Unmarshal(scanner.Bytes(), &request); err != nil { encoder.Encode(Response{Ok:false, Error:err.Error()}); continue }
94
+ encoder.Encode(parseRequest(request))
95
+ }
96
+ }
@@ -0,0 +1,91 @@
1
+ <?php
2
+ declare(strict_types=1);
3
+
4
+ $autoload = $argv[1] ?? '';
5
+ if (!$autoload || !is_file($autoload)) {
6
+ fwrite(STDOUT, json_encode(['ok' => false, 'error' => 'nikic/PHP-Parser autoload file not found']) . "\n");
7
+ exit(2);
8
+ }
9
+ require_once $autoload;
10
+
11
+ function node_name($node): string {
12
+ if ($node instanceof PhpParser\Node\Name || $node instanceof PhpParser\Node\Identifier || $node instanceof PhpParser\Node\VarLikeIdentifier) {
13
+ return $node->toString();
14
+ }
15
+ if ($node instanceof PhpParser\Node\Expr\Variable && is_string($node->name)) return '$' . $node->name;
16
+ if (is_string($node)) return $node;
17
+ return '';
18
+ }
19
+
20
+ function relation_target($node): string {
21
+ if ($node instanceof PhpParser\Node\Name || $node instanceof PhpParser\Node\Identifier) return $node->toString();
22
+ if ($node instanceof PhpParser\Node\Expr\Variable) return node_name($node);
23
+ if ($node instanceof PhpParser\Node\Expr\PropertyFetch || $node instanceof PhpParser\Node\Expr\StaticPropertyFetch) {
24
+ return trim(relation_target($node->var ?? $node->class) . '->' . node_name($node->name), '->');
25
+ }
26
+ return '';
27
+ }
28
+
29
+ function parser_instance() {
30
+ $factory = new PhpParser\ParserFactory();
31
+ if (method_exists($factory, 'createForNewestSupportedVersion')) return $factory->createForNewestSupportedVersion();
32
+ return $factory->create(PhpParser\ParserFactory::PREFER_PHP7);
33
+ }
34
+
35
+ function parse_source(string $source): array {
36
+ $declarations = []; $relations = []; $diagnostics = []; $next = 1;
37
+ try { $nodes = parser_instance()->parse($source) ?? []; }
38
+ catch (PhpParser\Error $error) {
39
+ return ['ok' => true, 'declarations' => [], 'relations' => [], 'diagnostics' => [[
40
+ 'line' => max(1, $error->getStartLine()), 'severity' => 'warning', 'kind' => 'parse_failed', 'message' => $error->getMessage()
41
+ ]]];
42
+ }
43
+ $walk = function ($node, string $owner = '') use (&$walk, &$declarations, &$relations, &$next): void {
44
+ if (!$node instanceof PhpParser\Node) return;
45
+ $current = $owner; $kind = null; $name = '';
46
+ if ($node instanceof PhpParser\Node\Stmt\Class_) { $kind = 'class'; $name = node_name($node->name) ?: 'anonymous'; }
47
+ elseif ($node instanceof PhpParser\Node\Stmt\Interface_) { $kind = 'interface'; $name = node_name($node->name); }
48
+ elseif ($node instanceof PhpParser\Node\Stmt\Trait_) { $kind = 'trait'; $name = node_name($node->name); }
49
+ elseif ($node instanceof PhpParser\Node\Stmt\Function_) { $kind = str_starts_with(strtolower(node_name($node->name)), 'test') ? 'test' : 'function'; $name = node_name($node->name); }
50
+ elseif ($node instanceof PhpParser\Node\Stmt\ClassMethod) { $kind = str_starts_with(strtolower(node_name($node->name)), 'test') ? 'test' : 'method'; $name = node_name($node->name); }
51
+ elseif ($node instanceof PhpParser\Node\Expr\Closure || $node instanceof PhpParser\Node\Expr\ArrowFunction) { $kind = 'function'; $name = 'anonymous'; }
52
+ if ($kind !== null) {
53
+ $current = (string)$next++;
54
+ $params = property_exists($node, 'params') ? array_map(fn($p) => node_name($p->var), $node->params) : [];
55
+ $visibility = method_exists($node, 'isPrivate') && $node->isPrivate() ? 'private' : (method_exists($node, 'isProtected') && $node->isProtected() ? 'protected' : (method_exists($node, 'isPublic') && $node->isPublic() ? 'public' : ''));
56
+ $declarations[] = ['key'=>$current,'parent'=>$owner,'kind'=>$kind,'name'=>$name,'line_start'=>max(1,$node->getStartLine()),'line_end'=>max(1,$node->getEndLine()),'signature'=>$name.'('.implode(', ',$params).')','visibility'=>$visibility,'native_kind'=>$node->getType()];
57
+ if ($node instanceof PhpParser\Node\Stmt\Class_) {
58
+ if ($node->extends) $relations[] = ['owner'=>$current,'type'=>'extends','target'=>'type:'.node_name($node->extends),'line'=>$node->extends->getStartLine(),'confidence'=>'inferred','metadata'=>['resolution'=>'syntax_name']];
59
+ foreach ($node->implements as $type) $relations[] = ['owner'=>$current,'type'=>'implements','target'=>'type:'.node_name($type),'line'=>$type->getStartLine(),'confidence'=>'inferred','metadata'=>['resolution'=>'syntax_name']];
60
+ }
61
+ }
62
+ if ($node instanceof PhpParser\Node\Stmt\Use_ || $node instanceof PhpParser\Node\Stmt\GroupUse) {
63
+ foreach ($node->uses as $use) $relations[] = ['owner'=>$owner,'type'=>'imports','target'=>'import:'.node_name($use->name),'line'=>$use->getStartLine(),'confidence'=>'exact','metadata'=>['resolution'=>'use_tree']];
64
+ } elseif ($node instanceof PhpParser\Node\Expr\FuncCall) {
65
+ $target = node_name($node->name); if ($target) $relations[] = ['owner'=>$owner,'type'=>'calls','target'=>'call:'.$target,'line'=>$node->getStartLine(),'confidence'=>'inferred','metadata'=>['resolution'=>'syntax_name']];
66
+ } elseif ($node instanceof PhpParser\Node\Expr\MethodCall || $node instanceof PhpParser\Node\Expr\NullsafeMethodCall) {
67
+ $target = node_name($node->name); if ($target) $relations[] = ['owner'=>$owner,'type'=>'calls','target'=>'call:'.$target,'line'=>$node->getStartLine(),'confidence'=>'heuristic','metadata'=>['resolution'=>'receiver_unresolved']];
68
+ } elseif ($node instanceof PhpParser\Node\Expr\StaticCall) {
69
+ $target = node_name($node->class).'::'.node_name($node->name); if ($target !== '::') $relations[] = ['owner'=>$owner,'type'=>'calls','target'=>'call:'.$target,'line'=>$node->getStartLine(),'confidence'=>'heuristic','metadata'=>['resolution'=>'static_receiver_unresolved']];
70
+ } elseif ($node instanceof PhpParser\Node\Expr\New_) {
71
+ $target = node_name($node->class); if ($target) $relations[] = ['owner'=>$owner,'type'=>'calls','target'=>'call:'.$target,'line'=>$node->getStartLine(),'confidence'=>'inferred','metadata'=>['resolution'=>'constructor_syntax']];
72
+ }
73
+ foreach ($node->getSubNodeNames() as $field) {
74
+ $value = $node->$field;
75
+ if ($value instanceof PhpParser\Node) $walk($value, $current);
76
+ elseif (is_array($value)) foreach ($value as $child) if ($child instanceof PhpParser\Node) $walk($child, $current);
77
+ }
78
+ };
79
+ foreach ($nodes as $node) $walk($node, '');
80
+ return ['ok'=>true,'declarations'=>$declarations,'relations'=>$relations,'diagnostics'=>$diagnostics];
81
+ }
82
+
83
+ while (($line = fgets(STDIN)) !== false) {
84
+ try {
85
+ $request = json_decode($line, true, 512, JSON_THROW_ON_ERROR);
86
+ echo json_encode(parse_source((string)($request['source'] ?? '')), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
87
+ } catch (Throwable $error) {
88
+ echo json_encode(['ok'=>false,'error'=>$error->getMessage()], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
89
+ }
90
+ flush();
91
+ }
@@ -0,0 +1,50 @@
1
+ // Optional helper for web-tree-sitter. It is intentionally dependency-free at
2
+ // package install time; the runtime reports parser_unavailable when the module
3
+ // or grammar is absent.
4
+ import fs from "node:fs";
5
+ import process from "node:process";
6
+
7
+ const grammarPath = process.argv[2];
8
+ const input = JSON.parse(fs.readFileSync(0, "utf8").trim());
9
+ try {
10
+ const mod = await import("web-tree-sitter");
11
+ const Parser = mod.default ?? mod.Parser;
12
+ const Language = mod.Language;
13
+ await Parser.init();
14
+ const language = await Language.load(grammarPath);
15
+ const parser = new Parser();
16
+ parser.setLanguage(language);
17
+ const tree = parser.parse(input.source);
18
+ const declarations = [];
19
+ const calls = [];
20
+ const declarationTypes = /(function|method|class|struct|interface|trait|enum|module|namespace|impl|object|type)/i;
21
+ const named = (node, field) => {
22
+ const child = node.childForFieldName?.(field);
23
+ return child ? child.text : "";
24
+ };
25
+ const walk = (node, parent = "") => {
26
+ const type = String(node.type || "");
27
+ const key = `${node.startIndex}:${node.endIndex}:${type}`;
28
+ if (declarationTypes.test(type) && !/(identifier|name|parameter)/i.test(type)) {
29
+ const name = named(node, "name") || named(node, "declarator") || `${type}@${node.startPosition.row + 1}`;
30
+ declarations.push({key, parent, kind: type, name, line_start: node.startPosition.row + 1, line_end: node.endPosition.row + 1});
31
+ parent = key;
32
+ }
33
+ if (/call|invocation|command_substitution/i.test(type)) {
34
+ const fn = named(node, "function") || named(node, "name") || node.firstNamedChild?.text || type;
35
+ calls.push({owner: parent, target: fn, line: node.startPosition.row + 1});
36
+ }
37
+ for (const child of node.namedChildren ?? []) walk(child, parent);
38
+ };
39
+ walk(tree.rootNode);
40
+ // web-tree-sitter exposes hasError as a boolean property (older releases
41
+ // exposed a method), so support both without treating a valid parse as a
42
+ // helper failure.
43
+ const hasErrorValue = tree.rootNode.hasError;
44
+ const hasError = typeof hasErrorValue === "function" ? hasErrorValue.call(tree.rootNode) : Boolean(hasErrorValue);
45
+ console.log(JSON.stringify({ok: true, parse_status: hasError ? "partial" : "complete", declarations, calls,
46
+ diagnostics: [], metadata: {grammar: grammarPath, error_tree: hasError}}));
47
+ } catch (error) {
48
+ console.log(JSON.stringify({ok: false, error: String(error?.message || error)}));
49
+ process.exitCode = 2;
50
+ }
@@ -0,0 +1,123 @@
1
+ const readline = require("readline");
2
+
3
+ let ts;
4
+ try {
5
+ ts = require("typescript");
6
+ } catch (error) {
7
+ process.stdout.write(JSON.stringify({ ok: false, error: `typescript compiler API unavailable: ${error.message}` }) + "\n");
8
+ process.exit(2);
9
+ }
10
+
11
+ function scriptKind(path) {
12
+ if (path.endsWith(".tsx")) return ts.ScriptKind.TSX;
13
+ if (path.endsWith(".jsx")) return ts.ScriptKind.JSX;
14
+ if (path.endsWith(".js") || path.endsWith(".mjs") || path.endsWith(".cjs")) return ts.ScriptKind.JS;
15
+ return ts.ScriptKind.TS;
16
+ }
17
+
18
+ function parse(input) {
19
+ const source = ts.createSourceFile(input.path, input.source, ts.ScriptTarget.Latest, true, scriptKind(input.path));
20
+ const declarations = [];
21
+ const relations = [];
22
+ let nextKey = 1;
23
+
24
+ function line(node) {
25
+ return source.getLineAndCharacterOfPosition(node.getStart(source, false)).line + 1;
26
+ }
27
+ function endLine(node) {
28
+ return source.getLineAndCharacterOfPosition(node.end).line + 1;
29
+ }
30
+ function nodeName(node) {
31
+ if (node.name && ts.isIdentifier(node.name)) return node.name.text;
32
+ if (node.name && ts.isStringLiteralLike(node.name)) return node.name.text;
33
+ return "anonymous";
34
+ }
35
+ function signature(node, name) {
36
+ if (node.parameters) {
37
+ const parameters = node.parameters.map((p) => p.name.getText(source));
38
+ return `${name}(${parameters.join(", ")})`;
39
+ }
40
+ return `${name}()`;
41
+ }
42
+ function visibility(node) {
43
+ const modifiers = node.modifiers || [];
44
+ if (modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword)) return "private";
45
+ if (modifiers.some((m) => m.kind === ts.SyntaxKind.ProtectedKeyword)) return "protected";
46
+ if (modifiers.some((m) => m.kind === ts.SyntaxKind.PublicKeyword)) return "public";
47
+ return "";
48
+ }
49
+ function declarationKind(node) {
50
+ if (ts.isClassDeclaration(node) || ts.isEnumDeclaration(node)) return "class";
51
+ if (ts.isInterfaceDeclaration(node)) return "interface";
52
+ if (ts.isModuleDeclaration(node)) return "module";
53
+ if (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isConstructorDeclaration(node)) return "method";
54
+ if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node)) return "function";
55
+ return null;
56
+ }
57
+ function callTarget(expression) {
58
+ if (ts.isIdentifier(expression)) return { target: expression.text, confidence: "inferred", resolution: "lexical_name" };
59
+ if (ts.isPropertyAccessExpression(expression)) return { target: expression.getText(source), confidence: "heuristic", resolution: "receiver_unresolved" };
60
+ if (ts.isElementAccessExpression(expression)) return { target: expression.getText(source), confidence: "heuristic", resolution: "dynamic_element" };
61
+ return null;
62
+ }
63
+ function addRelation(owner, type, target, at, confidence, metadata = {}) {
64
+ relations.push({ owner: owner || "", type, target, line: at, confidence, metadata });
65
+ }
66
+ function visit(node, owner = "") {
67
+ let current = owner;
68
+ let kind = declarationKind(node);
69
+ let name = nodeName(node);
70
+ if (ts.isVariableDeclaration(node) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
71
+ kind = "function";
72
+ name = node.name.getText(source);
73
+ }
74
+ if (kind) {
75
+ const key = String(nextKey++);
76
+ declarations.push({
77
+ key,
78
+ parent: owner,
79
+ kind: name.toLowerCase().startsWith("test") && kind === "function" ? "test" : kind,
80
+ name,
81
+ line_start: line(node),
82
+ line_end: endLine(node),
83
+ signature: signature(node, name),
84
+ visibility: visibility(node),
85
+ native_kind: ts.SyntaxKind[node.kind],
86
+ });
87
+ current = key;
88
+ const heritage = node.heritageClauses || [];
89
+ for (const clause of heritage) {
90
+ const type = clause.token === ts.SyntaxKind.ImplementsKeyword ? "implements" : "extends";
91
+ for (const inherited of clause.types) {
92
+ addRelation(current, type, `type:${inherited.expression.getText(source)}`, line(inherited), "inferred", { resolution: "syntax_name" });
93
+ }
94
+ }
95
+ }
96
+ if (ts.isImportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) {
97
+ addRelation(owner, "imports", `import:${node.moduleSpecifier.text}`, line(node), "exact", { resolution: "module_specifier" });
98
+ } else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) {
99
+ addRelation(owner, "imports", `import:${node.moduleSpecifier.text}`, line(node), "exact", { resolution: "export_module_specifier" });
100
+ } else if (ts.isCallExpression(node)) {
101
+ const target = callTarget(node.expression);
102
+ if (target) addRelation(owner, "calls", `call:${target.target}`, line(node), target.confidence, { resolution: target.resolution });
103
+ }
104
+ ts.forEachChild(node, (child) => visit(child, current));
105
+ }
106
+ visit(source, "");
107
+ const diagnostics = source.parseDiagnostics.map((diagnostic) => ({
108
+ line: diagnostic.start == null ? 1 : source.getLineAndCharacterOfPosition(diagnostic.start).line + 1,
109
+ severity: "warning",
110
+ kind: "parse_failed",
111
+ message: ts.flattenDiagnosticMessageText(diagnostic.messageText, " "),
112
+ }));
113
+ return { ok: true, declarations, relations, diagnostics };
114
+ }
115
+
116
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
117
+ rl.on("line", (line) => {
118
+ try {
119
+ process.stdout.write(JSON.stringify(parse(JSON.parse(line))) + "\n");
120
+ } catch (error) {
121
+ process.stdout.write(JSON.stringify({ ok: false, error: error.stack || error.message || String(error) }) + "\n");
122
+ }
123
+ });