devlensio 0.6.1 → 1.0.0

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 (129) hide show
  1. package/README.md +89 -31
  2. package/dist/extractors/detectLanguage.d.ts +2 -0
  3. package/dist/extractors/detectLanguage.js +33 -0
  4. package/dist/extractors/index.d.ts +6 -0
  5. package/dist/extractors/index.js +116 -0
  6. package/dist/extractors/runner.d.ts +7 -0
  7. package/dist/extractors/runner.js +194 -0
  8. package/dist/extractors/types.d.ts +37 -0
  9. package/dist/extractors/types.js +8 -0
  10. package/dist/graph/buildLookup.d.ts +1 -0
  11. package/dist/graph/buildLookup.js +2 -1
  12. package/dist/graph/edges/callEdges.js +19 -5
  13. package/dist/graph/edges/callEdges.test.d.ts +1 -0
  14. package/dist/graph/edges/callEdges.test.js +200 -0
  15. package/dist/graph/edges/importEdges.js +12 -0
  16. package/dist/graph/edges/inheritanceEdges.d.ts +3 -0
  17. package/dist/graph/edges/inheritanceEdges.js +73 -0
  18. package/dist/graph/edges/inheritanceEdges.test.d.ts +1 -0
  19. package/dist/graph/edges/inheritanceEdges.test.js +140 -0
  20. package/dist/graph/index.js +4 -0
  21. package/dist/parser/classes.test.d.ts +1 -0
  22. package/dist/parser/classes.test.js +360 -0
  23. package/dist/parser/extractors/classes.d.ts +5 -0
  24. package/dist/parser/extractors/classes.js +241 -0
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +7 -1
  27. package/dist/pipeline/index.d.ts +2 -1
  28. package/dist/pipeline/index.js +36 -48
  29. package/dist/scoring/index.js +8 -0
  30. package/dist/scoring/index.test.js +43 -0
  31. package/dist/scoring/nodeScorer.js +2 -0
  32. package/dist/scoring/pruneDisconnected.d.ts +8 -0
  33. package/dist/scoring/pruneDisconnected.js +66 -0
  34. package/dist/scoring/pruneDisconnected.test.d.ts +1 -0
  35. package/dist/scoring/pruneDisconnected.test.js +123 -0
  36. package/dist/summarizer/prompts.d.ts +1 -1
  37. package/dist/types.d.ts +5 -5
  38. package/extractors/go/bin/darwin-amd64/devlens_go_extractor +0 -0
  39. package/extractors/go/bin/darwin-arm64/devlens_go_extractor +0 -0
  40. package/extractors/go/bin/linux-amd64/devlens_go_extractor +0 -0
  41. package/extractors/go/bin/linux-arm64/devlens_go_extractor +0 -0
  42. package/extractors/go/bin/windows-amd64/devlens_go_extractor.exe +0 -0
  43. package/extractors/go/build.mjs +43 -0
  44. package/extractors/go/calls.go +289 -0
  45. package/extractors/go/contract.go +140 -0
  46. package/extractors/go/extractor.go +161 -0
  47. package/extractors/go/fingerprint.go +167 -0
  48. package/extractors/go/go.mod +3 -0
  49. package/extractors/go/imports.go +90 -0
  50. package/extractors/go/inheritance.go +138 -0
  51. package/extractors/go/lookup.go +106 -0
  52. package/extractors/go/main.go +57 -0
  53. package/extractors/go/nodes.go +177 -0
  54. package/extractors/go/orm_edges.go +277 -0
  55. package/extractors/go/parser.go +469 -0
  56. package/extractors/go/routes.go +521 -0
  57. package/extractors/go/tests.go +45 -0
  58. package/extractors/go/thirdparty.go +138 -0
  59. package/extractors/go/typeload.go +178 -0
  60. package/extractors/go/walker.go +67 -0
  61. package/extractors/java/build.mjs +90 -0
  62. package/extractors/java/devlens_java_extractor.jar +0 -0
  63. package/extractors/java/src/devlens/extractor/Contract.java +171 -0
  64. package/extractors/java/src/devlens/extractor/Extractor.java +262 -0
  65. package/extractors/java/src/devlens/extractor/ExtractorResult.java +12 -0
  66. package/extractors/java/src/devlens/extractor/Fingerprint.java +239 -0
  67. package/extractors/java/src/devlens/extractor/LookupMaps.java +123 -0
  68. package/extractors/java/src/devlens/extractor/Main.java +66 -0
  69. package/extractors/java/src/devlens/extractor/Parser.java +522 -0
  70. package/extractors/java/src/devlens/extractor/SourceWalker.java +82 -0
  71. package/extractors/java/src/devlens/extractor/ThirdParty.java +141 -0
  72. package/extractors/java/src/devlens/extractor/TypeSolverFactory.java +43 -0
  73. package/extractors/java/src/devlens/extractor/edges/Calls.java +266 -0
  74. package/extractors/java/src/devlens/extractor/edges/Enrich.java +54 -0
  75. package/extractors/java/src/devlens/extractor/edges/Imports.java +154 -0
  76. package/extractors/java/src/devlens/extractor/edges/Inheritance.java +79 -0
  77. package/extractors/java/src/devlens/extractor/edges/OrmEdges.java +209 -0
  78. package/extractors/java/src/devlens/extractor/edges/Routes.java +143 -0
  79. package/extractors/java/src/devlens/extractor/edges/Tests.java +53 -0
  80. package/extractors/python/devlens_extractors_python/__init__.py +3 -0
  81. package/extractors/python/devlens_extractors_python/__main__.py +33 -0
  82. package/extractors/python/devlens_extractors_python/contract.py +94 -0
  83. package/extractors/python/devlens_extractors_python/edges/__init__.py +28 -0
  84. package/extractors/python/devlens_extractors_python/edges/calls.py +112 -0
  85. package/extractors/python/devlens_extractors_python/edges/enrich.py +44 -0
  86. package/extractors/python/devlens_extractors_python/edges/imports.py +155 -0
  87. package/extractors/python/devlens_extractors_python/edges/inheritance.py +97 -0
  88. package/extractors/python/devlens_extractors_python/edges/orm_edges.py +225 -0
  89. package/extractors/python/devlens_extractors_python/edges/routes/__init__.py +28 -0
  90. package/extractors/python/devlens_extractors_python/edges/routes/common.py +79 -0
  91. package/extractors/python/devlens_extractors_python/edges/routes/decorators.py +212 -0
  92. package/extractors/python/devlens_extractors_python/edges/routes/django_urls.py +213 -0
  93. package/extractors/python/devlens_extractors_python/edges/routes/drf.py +128 -0
  94. package/extractors/python/devlens_extractors_python/edges/tests.py +53 -0
  95. package/extractors/python/devlens_extractors_python/extractor.py +107 -0
  96. package/extractors/python/devlens_extractors_python/fingerprint.py +201 -0
  97. package/extractors/python/devlens_extractors_python/lookup.py +72 -0
  98. package/extractors/python/devlens_extractors_python/parser/__init__.py +72 -0
  99. package/extractors/python/devlens_extractors_python/parser/classes.py +109 -0
  100. package/extractors/python/devlens_extractors_python/parser/functions.py +163 -0
  101. package/extractors/python/devlens_extractors_python/parser/walker.py +30 -0
  102. package/extractors/python/devlens_extractors_python/third_party.py +103 -0
  103. package/extractors/python/pyproject.toml +16 -0
  104. package/extractors/python/setup.mjs +54 -0
  105. package/extractors/rust/Cargo.toml +29 -0
  106. package/extractors/rust/bin/darwin-amd64/devlens_rust_extractor +0 -0
  107. package/extractors/rust/bin/darwin-arm64/devlens_rust_extractor +0 -0
  108. package/extractors/rust/bin/linux-amd64/devlens_rust_extractor +0 -0
  109. package/extractors/rust/bin/linux-arm64/devlens_rust_extractor +0 -0
  110. package/extractors/rust/bin/windows-amd64/devlens_rust_extractor.exe +0 -0
  111. package/extractors/rust/build.mjs +85 -0
  112. package/extractors/rust/src/calls.rs +295 -0
  113. package/extractors/rust/src/contract.rs +236 -0
  114. package/extractors/rust/src/enrich.rs +107 -0
  115. package/extractors/rust/src/extractor.rs +199 -0
  116. package/extractors/rust/src/fingerprint.rs +238 -0
  117. package/extractors/rust/src/imports.rs +101 -0
  118. package/extractors/rust/src/inheritance.rs +233 -0
  119. package/extractors/rust/src/lookup.rs +226 -0
  120. package/extractors/rust/src/main.rs +53 -0
  121. package/extractors/rust/src/module_map.rs +131 -0
  122. package/extractors/rust/src/nodes.rs +174 -0
  123. package/extractors/rust/src/orm_edges.rs +125 -0
  124. package/extractors/rust/src/parser.rs +859 -0
  125. package/extractors/rust/src/routes.rs +1265 -0
  126. package/extractors/rust/src/tests.rs +109 -0
  127. package/extractors/rust/src/thirdparty.rs +114 -0
  128. package/extractors/rust/src/walker.rs +82 -0
  129. package/package.json +22 -4
@@ -0,0 +1,79 @@
1
+ package devlens.extractor.edges;
2
+
3
+ import devlens.extractor.Contract;
4
+ import devlens.extractor.LookupMaps;
5
+ import devlens.extractor.Parser;
6
+ import devlens.extractor.ThirdParty;
7
+
8
+ import java.util.List;
9
+ import java.util.Map;
10
+
11
+ /**
12
+ * EXTENDS / IMPLEMENTS edge resolution (mirrors edges/inheritance.py).
13
+ *
14
+ * Java is the EASIEST of all languages here — explicit keywords, zero
15
+ * heuristics:
16
+ * extends Baz → EXTENDS (always)
17
+ * implements A, B → IMPLEMENTS (always; interfaces extending interfaces
18
+ * also use extends → EXTENDS)
19
+ *
20
+ * Targets resolve via symbol maps → same-package → closest-by-name. Local
21
+ * bases become class edges; gated third-party bases become [mvn] edges;
22
+ * unresolved bases are skipped — NEVER a dangling edge.
23
+ */
24
+ public final class Inheritance {
25
+
26
+ private Inheritance() {}
27
+
28
+ public static void resolve(List<Parser.ParsedFile> files, LookupMaps lookup,
29
+ ThirdParty tp, List<Map<String, Object>> edgesOut) {
30
+ for (Parser.ParsedFile pf : files) {
31
+ if (pf.isTest) {
32
+ continue;
33
+ }
34
+ String pkg = pf.packageName;
35
+ for (Parser.TypeInfo t : pf.allTypes()) {
36
+ String nodeId = pf.relPath + "::" + t.dottedName;
37
+ if (t.extendedType != null && !t.extendedType.isEmpty()) {
38
+ String target = resolveBase(t.extendedType, pf, lookup, tp);
39
+ if (target != null) {
40
+ edgesOut.add(Contract.edge("EXTENDS", nodeId, target, null));
41
+ }
42
+ }
43
+ for (String impl : t.implementedTypes) {
44
+ String target = resolveBase(impl, pf, lookup, tp);
45
+ if (target != null) {
46
+ edgesOut.add(Contract.edge("IMPLEMENTS", nodeId, target, null));
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+
53
+ private static String resolveBase(String simpleName, Parser.ParsedFile pf,
54
+ LookupMaps lookup, ThirdParty tp) {
55
+ String alias = lookup.symbolMap(pf.relPath).get(simpleName);
56
+ if (alias != null) {
57
+ // [mvn] aliases only exist when gated in (Imports checked `permitted`)
58
+ return alias;
59
+ }
60
+ if (!pf.packageName.isEmpty()) {
61
+ String fqcn = pf.packageName + "." + simpleName;
62
+ String rel = lookup.typeMap.get(fqcn);
63
+ if (rel != null) {
64
+ return rel + "::" + lookup.typeDottedMap.get(fqcn);
65
+ }
66
+ }
67
+ String closest = lookup.closestByName(simpleName, pf.relPath);
68
+ if (closest != null) {
69
+ Map<String, Object> node = lookup.nodeById.get(closest);
70
+ if (node != null) {
71
+ String type = (String) node.get("type");
72
+ if ("CLASS".equals(type) || "INTERFACE".equals(type) || "ENUM".equals(type)) {
73
+ return closest;
74
+ }
75
+ }
76
+ }
77
+ return null; // external/unresolved — skip, never dangling
78
+ }
79
+ }
@@ -0,0 +1,209 @@
1
+ package devlens.extractor.edges;
2
+
3
+ import devlens.extractor.Contract;
4
+ import devlens.extractor.LookupMaps;
5
+ import devlens.extractor.Parser;
6
+
7
+ import java.util.ArrayList;
8
+ import java.util.LinkedHashMap;
9
+ import java.util.List;
10
+ import java.util.Map;
11
+ import java.util.regex.Matcher;
12
+ import java.util.regex.Pattern;
13
+
14
+ /**
15
+ * JPA data layer (mirrors edges/orm_edges.py — highest fidelity of all
16
+ * languages per the playbook).
17
+ *
18
+ * Models: @Entity/@Table/@MappedSuperclass/@Embeddable → isModel
19
+ * (type "jpa"); @Document → "mongodb". fields[] from the class.
20
+ * Repos: interface extends JpaRepository<Entity, ID> etc → isRepository
21
+ * + linkedModel = the entity generic arg (via rawCode regex).
22
+ * R/W edges: consumer → store, same direction as stateEdges.ts —
23
+ * - repo's own derived methods (findByX, save, deleteByX) → R/W to entity
24
+ * - calls INTO repo methods (from services/controllers) → R/W to entity
25
+ * method-name grammar classifies: find|get|query|read|count|exists|select
26
+ * = READ; save|insert|update|delete|remove|persist = WRITE.
27
+ */
28
+ public final class OrmEdges {
29
+
30
+ private static final List<String> MODEL_ANNOTATIONS =
31
+ List.of("Entity", "Table", "MappedSuperclass", "Embeddable");
32
+ private static final List<String> REPO_TYPES = List.of(
33
+ "JpaRepository", "CrudRepository", "PagingAndSortingRepository",
34
+ "ListCrudRepository", "ListPagingAndSortingRepository",
35
+ "JpaSpecificationExecutor", "QuerydslPredicateExecutor",
36
+ "MongoRepository", "ReactiveCrudRepository", "R2dbcRepository",
37
+ "JdbcRepository", "ReactiveMongoRepository",
38
+ "Repository"); // Spring Data's base marker interface
39
+
40
+ private static final Pattern GENERIC_ARG =
41
+ Pattern.compile("extends\\s+[A-Za-z_][\\w.]*\\s*<\\s*([A-Za-z_][\\w.]*)");
42
+
43
+ private OrmEdges() {}
44
+
45
+ /**
46
+ * Detection pass — MUST run BEFORE Calls.resolve: model metadata
47
+ * (isModel/modelType/fields) + repository detection (isRepository,
48
+ * linkedModel) + derived-method R/W edges on repo interfaces.
49
+ */
50
+ public static void detect(LookupMaps lookup, List<Map<String, Object>> edgesOut) {
51
+ // ── 1. model metadata ─────────────────────────────────────────────
52
+ for (Map<String, Object> node : lookup.nodeById.values()) {
53
+ if (!"CLASS".equals(node.get("type"))) {
54
+ continue;
55
+ }
56
+ Map<String, Object> meta = (Map<String, Object>) node.get("metadata");
57
+ @SuppressWarnings("unchecked")
58
+ List<String> annotations = (List<String>) meta.get("annotations");
59
+ if (annotations == null) {
60
+ continue;
61
+ }
62
+ boolean isEntity = annotations.stream().anyMatch(MODEL_ANNOTATIONS::contains);
63
+ boolean isDocument = annotations.contains("Document");
64
+ if (isEntity || isDocument) {
65
+ meta.put("isModel", true);
66
+ meta.put("modelType", isDocument ? "mongodb" : "jpa");
67
+ // fields[] already on metadata from node build — expose explicitly
68
+ meta.putIfAbsent("fields", meta.get("fields") == null
69
+ ? new ArrayList<Map<String, Object>>() : meta.get("fields"));
70
+ }
71
+ }
72
+
73
+ // ── 2. repository detection + derived-method R/W edges ────────────
74
+ for (Map<String, Object> node : lookup.nodeById.values()) {
75
+ String type = (String) node.get("type");
76
+ if (!"INTERFACE".equals(type)) {
77
+ continue;
78
+ }
79
+ Map<String, Object> meta = (Map<String, Object>) node.get("metadata");
80
+ String ext = (String) meta.get("extendsType");
81
+ if (ext == null || REPO_TYPES.stream().noneMatch(ext::equals)) {
82
+ continue;
83
+ }
84
+ meta.put("isRepository", true);
85
+ String entityId = entityGenericArg(node, lookup);
86
+ if (entityId != null) {
87
+ meta.put("linkedModel", entityId);
88
+ // derived query methods on the repo itself
89
+ for (Map<String, Object> m : lookup.nodeById.values()) {
90
+ if (node.get("id").equals(parentClassId(m))) {
91
+ String methodName = (String) m.get("name");
92
+ String edgeType = readOrWrite(methodName);
93
+ if (edgeType != null) {
94
+ edgesOut.add(Contract.edge(edgeType, (String) node.get("id"), entityId,
95
+ Map.of("via", "repository-derived")));
96
+ }
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Consumer pass — AFTER Calls.resolve: calls into repo methods become
105
+ * READS_FROM/WRITES_TO edges from the calling class to the entity.
106
+ */
107
+ public static void consumerEdges(LookupMaps lookup, List<Map<String, Object>> edgesOut) {
108
+ for (Map.Entry<String, List<Parser.CallInfo>> entry : lookup.methodCallFacts.entrySet()) {
109
+ Map<String, Object> methodNode = lookup.nodeById.get(entry.getKey());
110
+ if (methodNode == null) {
111
+ continue;
112
+ }
113
+ @SuppressWarnings("unchecked")
114
+ List<Map<String, String>> details =
115
+ (List<Map<String, String>>) ((Map<String, Object>) methodNode.get("metadata"))
116
+ .get("resolvedCallDetails");
117
+ if (details == null) {
118
+ continue;
119
+ }
120
+ String ownerClassId = ownerClassId(methodNode);
121
+ for (Map<String, String> d : details) {
122
+ String targetClass = d.get("class");
123
+ Map<String, Object> targetNode = lookup.nodeById.get(targetClass);
124
+ if (targetNode == null) {
125
+ continue;
126
+ }
127
+ Map<String, Object> tmeta = (Map<String, Object>) targetNode.get("metadata");
128
+ if (!Boolean.TRUE.equals(tmeta.get("isRepository"))) {
129
+ continue;
130
+ }
131
+ String entityId = (String) tmeta.get("linkedModel");
132
+ if (entityId == null) {
133
+ continue;
134
+ }
135
+ String edgeType = readOrWrite(d.get("method"));
136
+ if (edgeType != null) {
137
+ edgesOut.add(Contract.edge(edgeType, ownerClassId, entityId,
138
+ Map.of("via", "repository-call")));
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ /** First generic arg of the extends clause: JpaRepository<User, Long> → User. */
145
+ private static String entityGenericArg(Map<String, Object> repoNode, LookupMaps lookup) {
146
+ String rawCode = (String) repoNode.get("rawCode");
147
+ if (rawCode == null) {
148
+ return null;
149
+ }
150
+ Matcher m = GENERIC_ARG.matcher(rawCode);
151
+ if (!m.find()) {
152
+ return null;
153
+ }
154
+ String entitySimple = Parser.simpleTypeName(m.group(1));
155
+ String relPath = (String) repoNode.get("filePath");
156
+ String pkg = packageOf(relPath);
157
+ return Calls.resolveTypeRef(entitySimple, lookup, relPath, pkg);
158
+ }
159
+
160
+ private static String readOrWrite(String methodName) {
161
+ if (methodName == null) {
162
+ return null;
163
+ }
164
+ // method node names are "Class.method" — classify on the method part
165
+ String n = methodName.contains(".")
166
+ ? methodName.substring(methodName.lastIndexOf('.') + 1) : methodName;
167
+ n = n.toLowerCase();
168
+ if (n.startsWith("find") || n.startsWith("get") || n.startsWith("query")
169
+ || n.startsWith("read") || n.startsWith("count") || n.startsWith("exists")
170
+ || n.startsWith("select") || n.startsWith("search")) {
171
+ return "READS_FROM";
172
+ }
173
+ if (n.startsWith("save") || n.startsWith("insert") || n.startsWith("update")
174
+ || n.startsWith("delete") || n.startsWith("remove") || n.startsWith("persist")
175
+ || n.startsWith("merge") || n.startsWith("flush")) {
176
+ return "WRITES_TO";
177
+ }
178
+ return null;
179
+ }
180
+
181
+ private static String parentClassId(Map<String, Object> methodNode) {
182
+ if (!"METHOD".equals(methodNode.get("type"))) {
183
+ return null;
184
+ }
185
+ String id = (String) methodNode.get("id");
186
+ int lastDot = id.lastIndexOf('.');
187
+ int colon = id.lastIndexOf("::");
188
+ return lastDot > colon ? id.substring(0, lastDot) : id;
189
+ }
190
+
191
+ private static String ownerClassId(Map<String, Object> methodNode) {
192
+ String id = parentClassId(methodNode);
193
+ return id == null ? (String) methodNode.get("id") : id;
194
+ }
195
+
196
+ private static String packageOf(String relPath) {
197
+ String p = relPath;
198
+ for (String root : new String[]{"src/main/java/", "src/test/java/"}) {
199
+ if (p.startsWith(root)) {
200
+ p = p.substring(root.length());
201
+ break;
202
+ }
203
+ }
204
+ if (p.endsWith(".java")) {
205
+ p = p.substring(0, p.length() - 5);
206
+ }
207
+ return p.replace('/', '.');
208
+ }
209
+ }
@@ -0,0 +1,143 @@
1
+ package devlens.extractor.edges;
2
+
3
+ import devlens.extractor.Contract;
4
+ import devlens.extractor.LookupMaps;
5
+ import devlens.extractor.Parser;
6
+
7
+ import java.util.ArrayList;
8
+ import java.util.HashSet;
9
+ import java.util.LinkedHashMap;
10
+ import java.util.List;
11
+ import java.util.Map;
12
+ import java.util.Set;
13
+ import java.util.regex.Matcher;
14
+ import java.util.regex.Pattern;
15
+
16
+ /**
17
+ * Spring MVC route detection (mirrors edges/routes/*.py).
18
+ *
19
+ * Annotation-driven — no type resolution needed:
20
+ * class-level @RestController / @Controller (+ @RequestMapping base path)
21
+ * method-level @GetMapping/@PostMapping/@PutMapping/@DeleteMapping/
22
+ * @PatchMapping/@RequestMapping(method = RequestMethod.X)
23
+ * Class prefix + method path compose; {id} path params → isDynamic + params.
24
+ *
25
+ * V1 scope: Spring MVC only. JAX-RS (@Path), @FeignClient, WebFlux
26
+ * functional routes are V2 (documented in java_graph_detection.html).
27
+ */
28
+ public final class Routes {
29
+
30
+ private static final Pattern PATH_PARAM = Pattern.compile("\\{([^}]+)\\}");
31
+
32
+ private static final List<String> MAPPING_ANNOTATIONS = List.of(
33
+ "RequestMapping", "GetMapping", "PostMapping", "PutMapping",
34
+ "DeleteMapping", "PatchMapping");
35
+
36
+ private Routes() {}
37
+
38
+ public static final class RouteResult {
39
+ public final List<Map<String, Object>> routes = new ArrayList<>();
40
+ public final List<Map<String, Object>> routeNodes = new ArrayList<>();
41
+ public final List<Map<String, Object>> handlesEdges = new ArrayList<>();
42
+ final Set<String> seen = new HashSet<>();
43
+ }
44
+
45
+ public static RouteResult resolve(List<Parser.ParsedFile> files, LookupMaps lookup,
46
+ String framework) {
47
+ RouteResult result = new RouteResult();
48
+ String fw = framework == null || framework.equals("unknown") ? "spring" : framework;
49
+ for (Parser.ParsedFile pf : files) {
50
+ if (pf.isTest) {
51
+ continue;
52
+ }
53
+ for (Parser.TypeInfo t : pf.allTypes()) {
54
+ if (!isController(t)) {
55
+ continue;
56
+ }
57
+ String base = pathValue(t.annotationValues.get("RequestMapping"));
58
+ for (Parser.MethodInfo m : t.methods) {
59
+ for (String ann : MAPPING_ANNOTATIONS) {
60
+ if (!m.annotations.contains(ann)) {
61
+ continue;
62
+ }
63
+ String methodPath = pathValue(m.annotationValues.get(ann));
64
+ String verb = verbFor(ann, m.annotationValues.get(ann + ".method"));
65
+ String urlPath = join(base, methodPath);
66
+ emit(pf, t, m, verb, urlPath, fw, result);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ return result;
72
+ }
73
+
74
+ private static boolean isController(Parser.TypeInfo t) {
75
+ return t.annotations.contains("RestController") || t.annotations.contains("Controller");
76
+ }
77
+
78
+ private static String verbFor(String ann, String methodAttr) {
79
+ if (!ann.equals("RequestMapping")) {
80
+ return ann.replace("Mapping", "").toUpperCase();
81
+ }
82
+ return methodAttr == null ? "ANY" : methodAttr.toUpperCase();
83
+ }
84
+
85
+ private static String pathValue(String raw) {
86
+ return raw == null ? "" : raw.trim();
87
+ }
88
+
89
+ private static String join(String base, String methodPath) {
90
+ String b = base == null ? "" : base;
91
+ String m = methodPath == null ? "" : methodPath;
92
+ if (b.isEmpty()) {
93
+ return m.isEmpty() ? "/" : normalize(m);
94
+ }
95
+ if (m.isEmpty()) {
96
+ return normalize(b);
97
+ }
98
+ if (m.startsWith("/")) {
99
+ m = m.substring(1);
100
+ }
101
+ return normalize(b + "/" + m);
102
+ }
103
+
104
+ private static String normalize(String p) {
105
+ String out = p;
106
+ if (!out.startsWith("/")) {
107
+ out = "/" + out;
108
+ }
109
+ while (out.endsWith("/") && out.length() > 1) {
110
+ out = out.substring(0, out.length() - 1);
111
+ }
112
+ return out;
113
+ }
114
+
115
+ private static void emit(Parser.ParsedFile pf, Parser.TypeInfo t, Parser.MethodInfo m,
116
+ String verb, String urlPath, String fw, RouteResult result) {
117
+ String key = pf.relPath + "|" + verb + "|" + urlPath;
118
+ if (!result.seen.add(key)) {
119
+ return;
120
+ }
121
+ Matcher matcher = PATH_PARAM.matcher(urlPath);
122
+ boolean dynamic = matcher.find();
123
+ List<String> params = new ArrayList<>();
124
+ matcher.reset();
125
+ while (matcher.find()) {
126
+ params.add(matcher.group(1));
127
+ }
128
+
129
+ String handlerId = pf.relPath + "::" + t.dottedName + "." + m.name;
130
+ Map<String, Object> routeNode = Contract.routeNode(pf.relPath, verb, urlPath,
131
+ dynamic, params, fw, m.name);
132
+ Map<String, Object> backendRoute = Contract.backendRoute(urlPath, pf.relPath,
133
+ verb, fw, dynamic, params, m.name, (String) routeNode.get("id"));
134
+
135
+ result.routeNodes.add(routeNode);
136
+ result.routes.add(backendRoute);
137
+ Map<String, Object> handlesMeta = new LinkedHashMap<>();
138
+ handlesMeta.put("urlPath", urlPath);
139
+ handlesMeta.put("httpMethod", verb);
140
+ handlesMeta.put("routeKind", "backend");
141
+ result.handlesEdges.add(Contract.edge("HANDLES", (String) routeNode.get("id"), handlerId, handlesMeta));
142
+ }
143
+ }
@@ -0,0 +1,53 @@
1
+ package devlens.extractor.edges;
2
+
3
+ import devlens.extractor.Contract;
4
+ import devlens.extractor.LookupMaps;
5
+
6
+ import java.util.ArrayList;
7
+ import java.util.List;
8
+ import java.util.Map;
9
+
10
+ /**
11
+ * TESTS edge resolution (mirrors edges/tests.py + JS testEdges).
12
+ *
13
+ * Test files are LEAF nodes — the parser still extracted their test methods
14
+ * (names land in metadata.testCases), but no test children enter the graph.
15
+ * TESTS edges point from the TEST file node → the production CLASS nodes it
16
+ * imports (symbol maps are the bridge — test files import production types).
17
+ */
18
+ public final class Tests {
19
+
20
+ private Tests() {}
21
+
22
+ public static void resolve(LookupMaps lookup, List<Map<String, Object>> edgesOut) {
23
+ for (Map.Entry<String, Map<String, Object>> entry : lookup.fileNodesByPath.entrySet()) {
24
+ Map<String, Object> fileNode = entry.getValue();
25
+ if (!"TEST".equals(fileNode.get("type"))) {
26
+ continue;
27
+ }
28
+ String relPath = entry.getKey();
29
+ // metadata.testCases = @Test methods extracted at parse time
30
+ List<String> cases = lookup.testCases.get(relPath);
31
+ if (cases != null && !cases.isEmpty()) {
32
+ ((Map<String, Object>) fileNode.get("metadata")).put("testCases", cases);
33
+ }
34
+ // production symbols this test file imports
35
+ Map<String, String> sm = lookup.symbolMap(relPath);
36
+ List<String> targets = new ArrayList<>(sm.values());
37
+ java.util.Collections.sort(targets);
38
+ for (String target : targets) {
39
+ if (target.startsWith("[mvn]")) {
40
+ continue; // don't test third-party libs
41
+ }
42
+ Map<String, Object> node = lookup.nodeById.get(target);
43
+ if (node == null) {
44
+ continue;
45
+ }
46
+ String type = (String) node.get("type");
47
+ if ("CLASS".equals(type) || "INTERFACE".equals(type) || "ENUM".equals(type)) {
48
+ edgesOut.add(Contract.edge("TESTS", "file::" + relPath, target, null));
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,3 @@
1
+ """DevLens Python extractor package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,33 @@
1
+ """Entry point — invoked as: python3 -m devlens_extractors_python
2
+
3
+ Reads the ExtractorInput JSON from stdin, runs extraction, writes the
4
+ ExtractorResult JSON to stdout. stdout must stay PURE JSON (the Node.js
5
+ orchestrator JSON.parses it verbatim) — all logging goes to stderr.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import sys
11
+
12
+ from .extractor import extract
13
+
14
+ def main() -> None:
15
+ raw = sys.stdin.read()
16
+
17
+ try:
18
+ payload = json.loads(raw)
19
+ except json.JSONDecodeError as exc:
20
+ print(f"devlens extractor: invalid JSON on stdin: {exc}", file=sys.stderr)
21
+ sys.exit(1)
22
+
23
+ repo_path = payload.get("repoPath")
24
+ if not repo_path:
25
+ print("devlens extractor: missing 'repoPath' in input", file=sys.stderr)
26
+ sys.exit(1)
27
+
28
+ options = payload.get("options") or {}
29
+ result = extract(repo_path, options)
30
+ sys.stdout.write(json.dumps(result) + "\n")
31
+
32
+ if __name__ == "__main__":
33
+ main()
@@ -0,0 +1,94 @@
1
+ """
2
+ contract Types - the JSON shape Devlens Engine expects.
3
+
4
+ Every key here is camel case to match the typescript interface in the devlens-engine. This file is the single
5
+ source of truth for the output shape; other modules never spell keys by hand.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ import hashlib
12
+ from typing import Any
13
+
14
+ def code_hash(raw_code: str) -> str:
15
+ """Return a SHA256 hash of the raw code string."""
16
+ return hashlib.sha256(raw_code.encode("utf-8")).hexdigest()[:16] # first 16 chars of the hash
17
+
18
+ @dataclass
19
+ class Fingerprint:
20
+ language: str = "python"
21
+ projectType: str = "unknown" # "backend" once a framework is detected
22
+ framework: str = "unknown" # "fastapi" | "flask" | "django" | "unknown"
23
+ router: str = "none"
24
+ stateManagement: list = field(default_factory=list) # always [] for Python
25
+ dataFetching: list = field(default_factory=list) # always [] for Python
26
+ databases: list = field(default_factory=list)
27
+ rawDependencies: dict = field(default_factory=dict)
28
+
29
+ def to_dict(self) -> dict:
30
+ return {
31
+ "language": self.language,
32
+ "projectType": self.projectType,
33
+ "framework": self.framework,
34
+ "router": self.router,
35
+ "stateManagement": self.stateManagement,
36
+ "dataFetching": self.dataFetching,
37
+ "databases": self.databases,
38
+ "rawDependencies": self.rawDependencies,
39
+ }
40
+
41
+ @dataclass
42
+ class Stats:
43
+ totalFiles: int = 0
44
+ totalNodes: int = 0
45
+ skippedFiles: int = 0
46
+
47
+ def to_dict(self) -> dict:
48
+ return {
49
+ "totalFiles": self.totalFiles,
50
+ "totalNodes": self.totalNodes,
51
+ "skippedFiles": self.skippedFiles,
52
+ }
53
+
54
+ @dataclass
55
+ class ExtractorError:
56
+ file: str
57
+ error: str
58
+
59
+ def to_dict(self) -> dict:
60
+ return {"file": self.file, "error": self.error}
61
+
62
+ def file_node(rel_path: str, end_line: int, node_type: str = "FILE",
63
+ language: str = "python") -> dict:
64
+ """FILE node — id format: file::rel/path.py (matches src/parser/index.ts)."""
65
+ return {
66
+ "id": f"file::{rel_path}",
67
+ "name": rel_path.split("/")[-1],
68
+ "type": node_type, # FILE | TEST
69
+ "filePath": rel_path,
70
+ "startLine": 1,
71
+ "endLine": end_line,
72
+ "parentFile": f"file::{rel_path}", # file nodes parent themselves
73
+ "metadata": {
74
+ "nodeCount": 0,
75
+ "childNodeIds": [],
76
+ "language": language,
77
+ },
78
+ }
79
+
80
+ def code_node(rel_path: str, name: str, node_type: str, start_line: int, end_line: int,
81
+ raw_code: str, metadata: dict, language: str = "python") -> dict:
82
+ """Function/Class/Method node. ID format: rel/path.py::name."""
83
+ return {
84
+ "id": f"{rel_path}::{name}",
85
+ "name": name,
86
+ "type": node_type,
87
+ "filePath": rel_path,
88
+ "startLine": start_line,
89
+ "endLine": end_line,
90
+ "rawCode": raw_code,
91
+ "codeHash": code_hash(raw_code),
92
+ "parentFile": f"file::{rel_path}",
93
+ "metadata": {"language": language, **metadata},
94
+ }
@@ -0,0 +1,28 @@
1
+ """Edge detectors — import resolution, call resolution, routes, ORM data layer.
2
+
3
+ Public API (all consume the shared LookupMaps built once by extractor.py):
4
+ resolve_file_imports(parsed, lookup, registry) -> (edges, imported_paths)
5
+ resolve_calls(lookup, registry) -> edges
6
+ detect_routes(parsed_files, lookup, fingerprint)-> RouteResult
7
+ detect_models(lookup) -> {model name → node id}
8
+ resolve_orm_edges(lookup, models) -> edges
9
+ """
10
+ from .calls import resolve_calls
11
+ from .enrich import enrich_metadata
12
+ from .imports import resolve_file_imports
13
+ from .inheritance import resolve_inheritance
14
+ from .orm_edges import detect_models, resolve_orm_edges
15
+ from .routes import RouteResult, detect_routes
16
+ from .tests import resolve_test_edges
17
+
18
+ __all__ = [
19
+ "resolve_file_imports",
20
+ "resolve_calls",
21
+ "detect_routes",
22
+ "RouteResult",
23
+ "detect_models",
24
+ "resolve_orm_edges",
25
+ "enrich_metadata",
26
+ "resolve_inheritance",
27
+ "resolve_test_edges",
28
+ ]