devlensio 0.6.2 → 1.0.1

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 +155 -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 +30 -42
  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
package/README.md CHANGED
@@ -3,7 +3,11 @@
3
3
  [![npm: devlensio](https://img.shields.io/badge/npm-devlensio-cb3837?logo=npm)](https://www.npmjs.com/package/devlensio)
4
4
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
5
5
 
6
- The core engine behind [DevLens](https://github.com/devlensio/devlensOSS) — the codebase visualizer. It turns a TypeScript / JavaScript / React / Next.js / Node.js repository into a **typed code graph** with functional summaries, technical summaries, and security analysis on every node.
6
+ The core engine behind [DevLens](https://github.com/devlensio/devlensOSS) — the codebase visualizer.
7
+
8
+ Point it at any repository and it builds a **typed code graph**: every file, function, class, route, and data model becomes a *node*, connected by *edges* that show what imports what, what calls what, and how data flows. Each node gets an importance score and — optionally — an AI-generated functional, technical, and security summary.
9
+
10
+ It understands **6 languages out of the box** — TypeScript, JavaScript, Python, Java, Go, and Rust — with framework-aware parsing (React, Next.js, Express, Django, FastAPI, Spring Boot, Gin, Axum, and more). No tree-sitter, no generic AST guesswork: every language uses its own mature, native parser.
7
11
 
8
12
  The user-facing tools — CLI, MCP server, Agent Skill, and Web UI — all live in [DevLens OSS](https://github.com/devlensio/devlensOSS) and consume this package.
9
13
 
@@ -11,34 +15,52 @@ The user-facing tools — CLI, MCP server, Agent Skill, and Web UI — all live
11
15
 
12
16
  ## What it does
13
17
 
18
+ Analyzing a repo runs through a simple 7-step pipeline:
19
+
14
20
  ```
15
21
  Repo path
16
22
  │
17
- [1] Fingerprint → detect language, framework, router, state manager, data layer
18
- [2] Route detection → extract routes (Next.js, React Router, Express, Fastify, Koa)
19
- [3] AST parsing → walk every .ts/.tsx/.js/.jsx → extract nodes with types
20
- [4] Edge detection → map CALLS, IMPORTS, PROP_PASS, WRITES_TO, and 12 more edge types
21
- [5] Scoring → multi-pass importance scoring (no AI, deterministic)
22
- [6] Clustering → assign cohesive clusters
23
- [7] Summarize (opt) → topological LLM summaries — functional + technical + security
23
+ [1] Fingerprint → which language & framework? (React, Express, Django, Spring, Gin, Axum …)
24
+ [2] Route detection → every URL the app serves, with the handler behind each route
25
+ [3] AST parsing → per-language parser walks the source → typed nodes (files, functions, classes, structs, traits …)
26
+ [4] Edge detection → links nodes: CALLS, IMPORTS, HANDLES, IMPLEMENTS, EXTENDS, READS_FROM, WRITES_TO + more
27
+ [5] Scoring → deterministic importance scoring (no AI)
28
+ [6] Clustering → groups related nodes into cohesive clusters
29
+ [7] Summarize (opt) → LLM summaries — functional + technical + security
24
30
  │
25
31
  ▼
26
- Graph saved to ~/.devlens → queried via the traversal API / CLI / MCP / UI
32
+ Graph saved to ~/.devlens → queried via the traversal API / CLI / MCP / UI
27
33
  ```
28
34
 
29
35
  Structural analysis is fast and deterministic. Summarization is the only step that calls an LLM — and unchanged nodes are reused across commits (90%+ free on re-runs).
30
36
 
31
37
  ---
32
38
 
39
+ ## Supported languages & frameworks
40
+
41
+ Every language uses a **native parser** — no tree-sitter. TypeScript/JavaScript is parsed inline by the engine; Python, Java, Go, and Rust each run a small language-native extractor that the engine orchestrates over JSON.
42
+
43
+ | Language | Extractor | Runtime needed on the analyzing machine | Frameworks detected (routes + data layer) |
44
+ | :-- | :-- | :-- | :-- |
45
+ | **TypeScript / JavaScript** | Inline (`ts-morph`) | none | React (incl. React Router), Next.js, Express, Fastify, Koa, Hono, Elysia, Bun |
46
+ | **Python** | Native (stdlib `ast`) | Python 3.11+ (private venv, auto-created) | Django, Flask, FastAPI, DRF · SQLAlchemy / Django ORM |
47
+ | **Java** | Native (JavaParser) | JVM 17+ | Spring Boot, Quarkus · JPA |
48
+ | **Go** | Native (prebuilt static binary) | none | net/http, Gin, Echo, Fiber · GORM, database/sql |
49
+ | **Rust** | Native (prebuilt static binary) | none | Axum, Actix-web, Rocket, utoipa-axum · Diesel |
50
+
51
+ **Zero-toolchain languages.** Go and Rust ship as prebuilt static binaries for Linux / macOS / Windows (amd64 + arm64), and the Java extractor ships as a prebuilt fat jar — no compiler, Maven, or Gradle needed where the analysis runs. Python creates a private `.venv` on install (idempotent, skipped if `python3` isn't found).
52
+
53
+ ---
54
+
33
55
  ## Install
34
56
 
35
57
  ```bash
36
- npm install devlensio
37
- # or
38
58
  bun add devlensio
59
+ # or
60
+ npm install devlensio
39
61
  ```
40
62
 
41
- Requires Node 18+ (or Bun). An LLM provider key is only needed for AI summarization — structural analysis works offline.
63
+ The engine runs on **Bun** — its published entry imports from `bun` (used by the job queue), so plain-Node loading isn't supported. No LLM key is needed for structural analysis — it's fully offline and deterministic. Keys are only needed for AI summaries. See the language table above for per-language runtime requirements.
42
64
 
43
65
  ---
44
66
 
@@ -105,23 +127,52 @@ Also exported: all core types (`CodeNode`, `CodeEdge`, `NodeType`, `EdgeType`, `
105
127
 
106
128
  ## Node & edge types
107
129
 
108
- **Node types**
109
-
110
- | Type | What it represents |
111
- | :-- | :-- |
112
- | `COMPONENT` | React / UI component |
113
- | `HOOK` | React custom hook |
114
- | `FUNCTION` | Plain function |
115
- | `STATE_STORE` | State management (Zustand, Redux, etc.) |
116
- | `UTILITY` | Utility / helper module |
117
- | `FILE` | File-level node |
118
- | `ROUTE` | Application route |
119
- | `TEST` | Test file |
120
- | `THIRD_PARTY` | External dependency |
130
+ **Node types** — everything the graph knows about
121
131
 
122
- **Edge types**
123
-
124
- `CALLS`, `IMPORTS`, `READS_FROM`, `WRITES_TO`, `PROP_PASS`, `EMITS`, `LISTENS`, `WRAPPED_BY`, `GUARDS`, `HANDLES`, `TESTS`, `USES`, `NEXTJS_API_CALL`, `NAVIGATES_TO`
132
+ | Type | What it means | Where you'll see it |
133
+ | :-- | :-- | :-- |
134
+ | `COMPONENT` | A React / UI component — something that renders UI | TS/JS |
135
+ | `HOOK` | A React custom hook (`useX`), including the state/functions it returns | TS/JS |
136
+ | `FUNCTION` | Any plain function — helper, callback, utility, serverless handler | TS/JS, Python, Go, Rust |
137
+ | `STATE_STORE` | A central state container (Zustand, Redux, …) | TS/JS |
138
+ | `UTILITY` | A helper module — code that isn't UI and isn't a component | TS/JS |
139
+ | `CLASS` | A class — a blueprint for objects (`class User {}`). JS ships props/state types & decorators for React class components | TS/JS, Python, Java |
140
+ | `METHOD` | A function attached to a class — shown as `ClassName.method` | TS/JS, Python, Java, Go, Rust |
141
+ | `INTERFACE` | A type contract — the shape implementing types must satisfy | Java, Go |
142
+ | `ENUM` | A fixed set of named values (e.g. `Status.Active`) | Java, Rust |
143
+ | `STRUCT` | A plain data structure with fields (record-like) | Go, Rust |
144
+ | `TRAIT` | Rust's version of an interface — a set of behaviors a type can implement | Rust |
145
+ | `IMPL_BLOCK` | A Rust `impl` block that adds methods & behavior to a type | Rust |
146
+ | `FILE` | One source file — the root node the rest of that file attaches to | every language |
147
+ | `ROUTE` | An application route — a URL the app serves, with the handler behind it | every language |
148
+ | `TEST` | A test file (`.test.tsx`, `_test.go`, `test_*.py`, …) | every language |
149
+ | `STORY` | A Storybook story file | TS/JS |
150
+ | `GHOST` | An invisible "event" node that ties event emitters to listeners via `EMITS` / `LISTENS` | TS/JS event graph |
151
+ | `THIRD_PARTY` | An external package/dependency — shown but not parsed | every language |
152
+ | `MODULE` / `PACKAGE` | Namespace / package node | reserved — not emitted yet |
153
+
154
+ **Edge types** — the arrows between nodes
155
+
156
+ | Edge | What it means | Where you'll see it |
157
+ | :-- | :-- | :-- |
158
+ | `CALLS` | A calls B — one function/method invokes another | all languages |
159
+ | `IMPORTS` | A file imports another file or package | all languages |
160
+ | `READS_FROM` | A reads data from B — a state store, model, or DB query (`User.objects.get`, `session.query(User)`, `db.Find`) | state + data layers (TS/JS, Python, Java, Go ORM) |
161
+ | `WRITES_TO` | A writes/updates data in B (`store.set(...)`, `user.save()`, `db.Create(...)`) | state + data layers (same as above) |
162
+ | `PROP_PASS` | A React prop flows from a parent component to a child | TS/JS |
163
+ | `EMITS` | A emits an event | TS/JS event graph |
164
+ | `LISTENS` | A subscribes to an event | TS/JS event graph |
165
+ | `WRAPPED_BY` | A is wrapped by B (e.g. a context provider wraps its consumers) | TS/JS |
166
+ | `GUARDS` | A guards B — a route guard / middleware protecting a route or handler | route layers |
167
+ | `HANDLES` | A route is handled by a handler — the controller/viewset/function behind a URL | all languages' routes |
168
+ | `TESTS` | A test file verifies the code it points to | all languages |
169
+ | `USES` | A JSX component uses an external function/hook internally | TS/JS |
170
+ | `NEXTJS_API_CALL` | A component fetches a Next.js API route | TS/JS Next.js |
171
+ | `NAVIGATES_TO` | Client-side navigation points to a route | TS/JS |
172
+ | `IMPLEMENTS` | A type implements a contract — class implements an interface, Go struct implements an interface, Rust type implements a trait | Java, Go, Rust, Python (ABC/Protocol) |
173
+ | `EXTENDS` | A inherits / embeds B — class extends a base class, Go struct embeds another, Rust supertrait | class-based languages |
174
+ | `EXPORTS` | reserved — declared, not yet emitted | — |
175
+ | `THROWS` | reserved — declared, not yet emitted | — |
125
176
 
126
177
  Each node carries: **importance score** + **functional summary** + **technical summary** + **security assessment** (severity + notes).
127
178
 
@@ -236,9 +287,10 @@ A custom-model entry is always available — essential for OpenRouter's huge cat
236
287
  src/
237
288
  ├── fingerprint/ # Detect framework, language, router, state, data layer
238
289
  ├── filesystem/ # Route detection (Next.js, React Router, Express, etc.)
239
- ├── parser/ # AST extraction → nodes (ts-morph)
290
+ ├── parser/ # AST extraction → nodes (ts-morph) — the inline JS/TS extractor
291
+ ├── extractors/ # Extractor registry, language detection, subprocess runner
240
292
  ├── graph/ # Edge detectors, traversal API, lookup maps
241
- ├── scoring/ # Multi-pass importance scoring + noise filtering
293
+ ├── scoring/ # Multi-pass importance scoring + noise filtering + pruning
242
294
  ├── clustering/ # Cohesive cluster computation
243
295
  ├── summarizer/ # LLM summarization pipeline, prompts, checkpoints
244
296
  │ └── providers/ # Generic OpenAI & Anthropic clients, model discovery
@@ -247,7 +299,13 @@ src/
247
299
  ├── storage/ # File-based graph persistence (~/.devlens)
248
300
  ├── config/ # Provider config resolution (types, catalog, writer, env)
249
301
  ├── server/ # HTTP API server (consumed by Web UI)
250
- └── debug/ # Export and dev utilities
302
+ └── debug/ # Export and validation utilities
303
+
304
+ extractors/ # Native subprocess extractors (not part of the TS build)
305
+ ├── python/ # Python extractor (stdlib `ast`, pip package, auto-venv)
306
+ ├── java/ # Java extractor (JavaParser, prebuilt fat jar)
307
+ ├── go/ # Go extractor (go/ast + go/types, prebuilt static binary)
308
+ └── rust/ # Rust extractor (syn, prebuilt static binary)
251
309
  ```
252
310
 
253
311
  ---
@@ -0,0 +1,2 @@
1
+ import { Language } from "../index.js";
2
+ export declare function detectLanguage(repoPath: string): Language;
@@ -0,0 +1,33 @@
1
+ // Detects the primary langauge of a repo by checking for the manifest files
2
+ import fs from "fs";
3
+ import path from "path";
4
+ export function detectLanguage(repoPath) {
5
+ // first for JS/TS, check for package.json first then use tsconfig.json to distinguish between JS and TS
6
+ if (fs.existsSync(`${repoPath}/package.json`)) {
7
+ if (fs.existsSync(`${repoPath}/tsconfig.json`)) {
8
+ return "typescript";
9
+ }
10
+ return "javascript";
11
+ }
12
+ // secondly lets check for python.
13
+ if (fs.existsSync(path.join(repoPath, "requirements.txt")) ||
14
+ fs.existsSync(path.join(repoPath, "pyproject.toml")) ||
15
+ fs.existsSync(path.join(repoPath, "setup.py"))) {
16
+ return "python";
17
+ }
18
+ // Go
19
+ if (fs.existsSync(path.join(repoPath, "go.mod"))) {
20
+ return "go";
21
+ }
22
+ // Rust
23
+ if (fs.existsSync(path.join(repoPath, "Cargo.toml"))) {
24
+ return "rust";
25
+ }
26
+ // Java
27
+ if (fs.existsSync(path.join(repoPath, "pom.xml")) ||
28
+ fs.existsSync(path.join(repoPath, "build.gradle")) ||
29
+ fs.existsSync(path.join(repoPath, "build.gradle.kts"))) {
30
+ return "java";
31
+ }
32
+ return "unknown";
33
+ }
@@ -0,0 +1,6 @@
1
+ import { Language } from "../types.js";
2
+ import { ExtractorResult, LanguageExtractor } from "./types.js";
3
+ export declare function defaultParseResult(stdout: string): ExtractorResult;
4
+ export declare function getExtractor(language: Language): LanguageExtractor | undefined;
5
+ export declare const INLINE_LANGUAGES: Set<Language>;
6
+ export declare function commandExists(command: string): boolean;
@@ -0,0 +1,155 @@
1
+ // src/extractors/index.ts
2
+ import { fileURLToPath } from "url";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ // runner.ts is the main file entry point for the extractors. It contains the runExtractor() function which is called by the pipeline.
6
+ // Extractor registry - maps langauge to their extractor config.
7
+ // JS/TS is NOT here (it will be handled inline by the runner.ts)
8
+ // All subprocesses extractors must return the JSON (ExtractorResults) format
9
+ export function defaultParseResult(stdout) {
10
+ return JSON.parse(stdout);
11
+ }
12
+ // Subprocess Extractor Registry
13
+ // These extractors are spawned as child processes.
14
+ // ── extractor artifact resolution ────────────────────────────────────────────
15
+ // The subprocess extractors (python venv, java jar, go/rust static binaries)
16
+ // are DATA files inside the devlensio package. In a normal install they sit at
17
+ // `<pkg>/extractors/…` and resolve relative to this module. But when devlensio
18
+ // is BUNDLED into a standalone binary (`bun build --compile`, which is how the
19
+ // `@devlensio/cli` and MCP server ship), `import.meta.url` points at the binary
20
+ // itself, so the URL-relative path resolves to `/extractors/…` (nonexistent).
21
+ // The fallbacks below find the real extractor root for that case.
22
+ function resolveExtractorsRoot() {
23
+ // 1. Env override (CI / unusual layouts): DEVLENS_EXTRACTORS_DIR = the
24
+ // `extractors/` directory itself.
25
+ const env = process.env.DEVLENS_EXTRACTORS_DIR;
26
+ if (env) {
27
+ const candidate = path.resolve(env);
28
+ if (fs.existsSync(path.join(candidate, "python")) && fs.existsSync(path.join(candidate, "java"))) {
29
+ return candidate;
30
+ }
31
+ }
32
+ // 2. Normal install: `<pkg>/dist/extractors/../../extractors` = `<pkg>/extractors`.
33
+ const viaModule = fileURLToPath(new URL("../../extractors", import.meta.url));
34
+ if (fs.existsSync(viaModule))
35
+ return viaModule;
36
+ // 3. Bundled binary: walk up from this bundle's own directory AND from the
37
+ // cwd to find `<…>/node_modules/devlensio/extractors`. Covers running the
38
+ // compiled CLI inside a project that depends on devlensio, and global
39
+ // installs where devlensio sits hoisted beside the CLI package.
40
+ const scanRoots = [
41
+ fileURLToPath(new URL(".", import.meta.url)),
42
+ process.cwd(),
43
+ ];
44
+ for (const start of scanRoots) {
45
+ let dir = path.resolve(start);
46
+ for (;;) {
47
+ const candidate = path.join(dir, "node_modules", "devlensio", "extractors");
48
+ if (fs.existsSync(candidate))
49
+ return candidate;
50
+ const parent = path.dirname(dir);
51
+ if (parent === dir)
52
+ break;
53
+ dir = parent;
54
+ }
55
+ }
56
+ return null;
57
+ }
58
+ const extractorsRoot = resolveExtractorsRoot();
59
+ /** Absolute path to an extractor artifact under the resolved root (or null). */
60
+ function extractorArtifact(rel) {
61
+ return extractorsRoot ? path.join(extractorsRoot, rel) : null;
62
+ }
63
+ function resolvePythonCommand() {
64
+ const venvBin = process.platform === "win32" ? "Scripts/python.exe" : "bin/python";
65
+ const venvPython = extractorArtifact(`python/.venv/${venvBin}`) ??
66
+ // Last resort: URL-relative (pre-fix behavior for direct-from-dist runs).
67
+ fileURLToPath(new URL(`../../extractors/python/.venv/${venvBin}`, import.meta.url));
68
+ return fs.existsSync(venvPython) ? venvPython : "python3";
69
+ }
70
+ function platformDir() {
71
+ return process.platform === "win32"
72
+ ? "windows-amd64"
73
+ : process.platform === "darwin"
74
+ ? `darwin-${process.arch === "arm64" ? "arm64" : "amd64"}`
75
+ : `linux-${process.arch === "arm64" ? "arm64" : "amd64"}`;
76
+ }
77
+ function resolveJavaJarPath() {
78
+ const rel = "java/devlens_java_extractor.jar";
79
+ return extractorArtifact(rel) ?? fileURLToPath(new URL(`../../extractors/${rel}`, import.meta.url));
80
+ }
81
+ function resolveGoBinaryPath() {
82
+ const exe = process.platform === "win32" ? ".exe" : "";
83
+ const rel = `go/bin/${platformDir()}/devlens_go_extractor${exe}`;
84
+ return extractorArtifact(rel) ?? fileURLToPath(new URL(`../../extractors/${rel}`, import.meta.url));
85
+ }
86
+ function resolveRustBinaryPath() {
87
+ const exe = process.platform === "win32" ? ".exe" : "";
88
+ const rel = `rust/bin/${platformDir()}/devlens_rust_extractor${exe}`;
89
+ return extractorArtifact(rel) ?? fileURLToPath(new URL(`../../extractors/${rel}`, import.meta.url));
90
+ }
91
+ const SUBPROCESS_EXTRACTORS = {
92
+ python: {
93
+ language: "python",
94
+ // Absolute venv python when the postinstall created one (works from
95
+ // node_modules); fall back to PATH python3 in dev/other setups.
96
+ command: resolvePythonCommand(),
97
+ args: ["-m", "devlens_extractors_python"],
98
+ parseResult: defaultParseResult,
99
+ },
100
+ java: {
101
+ language: "java",
102
+ command: "java",
103
+ // Absolute path — the runner spawns with cwd=repoPath, so a bare jar
104
+ // name would be looked up inside the analyzed repo. fileURLToPath
105
+ // also decodes percent-escapes (spaces in the path).
106
+ args: ["-jar", resolveJavaJarPath()],
107
+ parseResult: defaultParseResult,
108
+ },
109
+ go: {
110
+ language: "go",
111
+ // Absolute per-platform static binary path — the runner spawns with
112
+ // cwd=repoPath, so a bare name would be looked up inside the analyzed
113
+ // repo. Cross-compiled at publish time by prepack → build.mjs.
114
+ command: resolveGoBinaryPath(),
115
+ args: [],
116
+ parseResult: defaultParseResult,
117
+ },
118
+ rust: {
119
+ language: "rust",
120
+ // Absolute per-platform static binary path — the runner spawns with
121
+ // cwd=repoPath, so a bare name would be looked up inside the analyzed
122
+ // repo. Cross-compiled at publish time by prepack → build.mjs.
123
+ command: resolveRustBinaryPath(),
124
+ args: [],
125
+ parseResult: defaultParseResult,
126
+ }
127
+ };
128
+ // Public API to get the extractor config
129
+ export function getExtractor(language) {
130
+ return SUBPROCESS_EXTRACTORS[language];
131
+ }
132
+ // langauges handled inline meaning JS/TS
133
+ export const INLINE_LANGUAGES = new Set(["javascript", "typescript"]);
134
+ // Does `command` resolve? Absolute path → existsSync; bare name → PATH scan
135
+ // (cross-platform: PATHEXT on win32).
136
+ export function commandExists(command) {
137
+ if (command.includes("/") || command.includes("\\")) {
138
+ return fs.existsSync(command);
139
+ }
140
+ const pathEnv = process.env.PATH || "";
141
+ const exts = process.platform === "win32"
142
+ ? (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").toLowerCase().split(";")
143
+ : [""];
144
+ for (const dir of pathEnv.split(process.platform === "win32" ? ";" : ":")) {
145
+ if (!dir)
146
+ continue;
147
+ for (const ext of exts) {
148
+ if (fs.existsSync(path.join(dir, command + ext))
149
+ || fs.existsSync(path.join(dir, command.toUpperCase() + ext))) {
150
+ return true;
151
+ }
152
+ }
153
+ }
154
+ return false;
155
+ }
@@ -0,0 +1,7 @@
1
+ import { ExtractorInput, ExtractorResult, LanguageExtractor } from "./types.js";
2
+ export declare function runSubprocessExtractor(extractor: LanguageExtractor, input: ExtractorInput, timeoutMs?: number): Promise<ExtractorResult>;
3
+ export declare function runInlineExtractor(input: ExtractorInput, onStep?: (step: "fingerprint" | "filesystem" | "parse" | "edges" | "scoring") => void): Promise<ExtractorResult>;
4
+ export declare function runExtractor(repoPath: string, options?: {
5
+ includeThirdPartyLibs?: string[];
6
+ onStep?: (step: "fingerprint" | "filesystem" | "parse" | "edges" | "scoring") => void;
7
+ }): Promise<ExtractorResult>;
@@ -0,0 +1,194 @@
1
+ // src/extractors/runner.ts
2
+ //
3
+ // The Runner — two execution modes:
4
+ // 1. Subprocess: spawns Python/Java/Go/Rust extractor as child process
5
+ // 2. Inline: calls existing ts-morph code directly (no subprocess)
6
+ //
7
+ // The pipeline calls runExtractor() which auto-detects language and routes.
8
+ // This is basically the entry point for the extractor execution.
9
+ /*
10
+ What Actually Happens (Timeline)
11
+ spawn() creates the child process and returns immediately. The child is now running but waiting for input on stdin. Meanwhile, your Node.js code continues executing line by line.
12
+
13
+ Here's the real execution order:
14
+
15
+
16
+ TIME 0ms: spawn("python3", ["-m", "devlens_extractors_python"])
17
+ → child process starts, waits for stdin input
18
+
19
+ TIME 1ms: child.stdout.on("data", ...) ← registers callback (doesn't run yet)
20
+ TIME 2ms: child.stderr.on("data", ...) ← registers callback (doesn't run yet)
21
+ TIME 3ms: setTimeout(...) ← registers timer (doesn't fire yet)
22
+ TIME 4ms: child.on("error", ...) ← registers callback
23
+ TIME 5ms: child.on("close", ...) ← registers callback
24
+
25
+ TIME 6ms: child.stdin.write(JSON.stringify(input)) ← NOW we send the repoPath
26
+ TIME 7ms: child.stdin.end() ← tells extractor "done sending"
27
+
28
+ ← runSubprocessExtractor returns the Promise here
29
+ ← Node.js moves on to other work
30
+
31
+ ... child process is running, parsing the repo ...
32
+
33
+ TIME 5000ms: child finishes, writes JSON to stdout
34
+ → "data" handler fires, collects stdout
35
+
36
+ TIME 5001ms: child exits with code 0
37
+ → "close" handler fires
38
+ → parseResult(stdout) runs
39
+ → resolve(result) — the Promise resolves
40
+ */
41
+ import { spawn } from "child_process";
42
+ import fs from "fs";
43
+ import path from "path";
44
+ import { analyzeFingerprint } from "../fingerprint/index.js";
45
+ import { analyzeFilesystem } from "../filesystem/index.js";
46
+ import { routesToCodeNodes } from "../pipeline/index.js";
47
+ import { parseRepo } from "../parser/index.js";
48
+ import { buildThirdPartyNodes } from "../graph/thirdPartyLibs.js";
49
+ import { detectEdges } from "../graph/index.js";
50
+ import { detectLanguage } from "./detectLanguage.js";
51
+ import { getExtractor, INLINE_LANGUAGES, commandExists } from "./index.js";
52
+ // 1. Subprocess Extractor
53
+ // What it does: It will start a process for the given extractor, send it the input json, and wait for the output json. if the process times out, it will kill the process and return error result. If the process exits with non-zero code, it will return error result. If the process exits with zero code, it will parse the output json and return the result.
54
+ export async function runSubprocessExtractor(extractor, input, timeoutMs = 10 * 60 * 1000) {
55
+ return new Promise((resolve, reject) => {
56
+ // Friendly guard for artifact-based extractors (java -jar ...):
57
+ // the jar path is resolved from the package location, not repoPath.
58
+ const jarArg = extractor.args.find((a) => a.endsWith(".jar"));
59
+ if (jarArg && !fs.existsSync(jarArg)) {
60
+ reject(new Error(`${extractor.language} extractor artifact not found: ${jarArg}. ` +
61
+ `Build it first: node extractors/${extractor.language}/build.mjs`));
62
+ return;
63
+ }
64
+ // Runtime prerequisites on the installing machine — friendly errors
65
+ // instead of a raw spawn ENOENT.
66
+ if (extractor.language === "java" && !commandExists("java")) {
67
+ reject(new Error("java extractor requires a Java 17+ runtime (JVM) on PATH — " +
68
+ "install a JDK (e.g. Adoptium Temurin) and retry."));
69
+ return;
70
+ }
71
+ if (extractor.language === "python" && !commandExists(extractor.command)) {
72
+ reject(new Error("python extractor unavailable: no Python 3.11+ found. Install Python, " +
73
+ "or bootstrap the extractor venv: node extractors/python/setup.mjs"));
74
+ return;
75
+ }
76
+ if (extractor.language === "go" && !fs.existsSync(extractor.command)) {
77
+ reject(new Error(`go extractor binary not found: ${extractor.command}. ` +
78
+ `Build it first: node extractors/go/build.mjs (requires the Go toolchain)`));
79
+ return;
80
+ }
81
+ if (extractor.language === "rust" && !fs.existsSync(extractor.command)) {
82
+ reject(new Error(`rust extractor binary not found: ${extractor.command}. ` +
83
+ `Build it first: node extractors/rust/build.mjs (requires the Rust toolchain)`));
84
+ return;
85
+ }
86
+ const child = spawn(extractor.command, extractor.args, { stdio: ["pipe", "pipe", "pipe"], cwd: input.repoPath });
87
+ let stdout = "";
88
+ let stderr = "";
89
+ child.stdout.on("data", (data) => {
90
+ stdout += data.toString();
91
+ });
92
+ child.stderr.on("data", (data) => {
93
+ stderr += data.toString();
94
+ });
95
+ const timer = setTimeout(() => {
96
+ child.kill("SIGTERM");
97
+ reject(new Error(`${extractor.language} extractor timed out after ${timeoutMs}ms`));
98
+ }, timeoutMs);
99
+ child.on("close", (code) => {
100
+ clearTimeout(timer);
101
+ if (code !== 0) {
102
+ // Log stderr for debugging but don't crash — return error result
103
+ reject(new Error(`${extractor.language} extractor exited with code ${code}.\n${stderr}`));
104
+ return;
105
+ }
106
+ try {
107
+ const result = extractor.parseResult(stdout);
108
+ resolve(result);
109
+ }
110
+ catch (err) {
111
+ reject(new Error(`Failed to parse ${extractor.language} extractor output: ` +
112
+ `${err instanceof Error ? err.message : String(err)}`));
113
+ }
114
+ });
115
+ // Send input as JSON on stdin, then close stdin to signal "done sending"
116
+ child.stdin.write(JSON.stringify(input));
117
+ child.stdin.end();
118
+ });
119
+ }
120
+ ;
121
+ // 2. Inline Extractor
122
+ // calls existing ts-morph code directly (no subprocess). This is used for TypeScript/JavaScript projects. It returns the result directly without spawning a child process.
123
+ // This replicates steps 1-6 of the current analyzePipeline and wraps them
124
+ // into an ExtractorResult.
125
+ export async function runInlineExtractor(input, onStep) {
126
+ const { repoPath, options } = input;
127
+ const absoluteRepoPath = path.resolve(repoPath);
128
+ // Step 1: fingerprint
129
+ onStep?.("fingerprint");
130
+ const fingerprint = analyzeFingerprint(absoluteRepoPath);
131
+ // Step 2: Filesystem / routes
132
+ onStep?.("filesystem");
133
+ const routes = analyzeFilesystem(absoluteRepoPath, fingerprint);
134
+ // Step 3: Convert routes -> CodeNodes (so that they can join the graph)
135
+ // (no onStep here — it's part of the filesystem step)
136
+ let routeNodes = routesToCodeNodes(routes, absoluteRepoPath);
137
+ // Step 4: Parse source files into nodes
138
+ onStep?.("parse");
139
+ const parserResult = parseRepo(absoluteRepoPath);
140
+ // Step 5: Build Third party nodes (if options is provided)
141
+ const thirdPartyNodes = buildThirdPartyNodes(absoluteRepoPath, options.includeThirdPartyLibs || []);
142
+ //Step 6: We have all the Nodes, now build the edges.
143
+ onStep?.("edges");
144
+ const edgeResult = detectEdges([...parserResult.nodes, ...routeNodes, ...thirdPartyNodes], routes, absoluteRepoPath, fingerprint);
145
+ // Step 7: Filter API route nodes without handlers (JS-specific cleanup)
146
+ routeNodes = routeNodes.filter(routeNode => {
147
+ if (routeNode.metadata.routeNodeType === "API_ROUTE") {
148
+ return edgeResult.edges.some((edge) => edge.type === "HANDLES" && edge.from === routeNode.id);
149
+ }
150
+ return true;
151
+ });
152
+ // Step 8: Assemble final nodes and edges
153
+ const allNodes = [
154
+ ...parserResult.nodes,
155
+ ...routeNodes,
156
+ ...thirdPartyNodes,
157
+ ...edgeResult.ghostNodes
158
+ ];
159
+ const allEdges = edgeResult.edges;
160
+ const stats = {
161
+ totalFiles: parserResult.stats.totalFiles,
162
+ totalNodes: allNodes.length,
163
+ skippedFiles: parserResult.stats.skippedFiles,
164
+ };
165
+ return {
166
+ fingerprint,
167
+ nodes: allNodes,
168
+ edges: allEdges,
169
+ routes,
170
+ stats,
171
+ errors: [],
172
+ };
173
+ }
174
+ // 3. Dispatch - auto detect other langauges (apart from JS/TS) and routes
175
+ export async function runExtractor(repoPath, options) {
176
+ const language = detectLanguage(repoPath);
177
+ const input = {
178
+ repoPath,
179
+ options: {
180
+ includeThirdPartyLibs: options?.includeThirdPartyLibs || []
181
+ }
182
+ };
183
+ if (INLINE_LANGUAGES.has(language)) {
184
+ return runInlineExtractor(input, options?.onStep);
185
+ }
186
+ // For other languages (python, Go, Rust, Java), we need to spawn a subprocess
187
+ const extractor = getExtractor(language);
188
+ if (!extractor) {
189
+ throw new Error(`No extractor registered for language: "${language}". ` +
190
+ `This language may not be supported yet.`);
191
+ }
192
+ ;
193
+ return runSubprocessExtractor(extractor, input);
194
+ }
@@ -0,0 +1,37 @@
1
+ import { Language, BackendRouteNode, CodeEdge, CodeNode, ProjectFingerprint, RouteNode } from "../types.js";
2
+ export interface ExtractorOptions {
3
+ includeThirdPartyLibs?: string[];
4
+ }
5
+ export interface ExtractorInput {
6
+ repoPath: string;
7
+ options: ExtractorOptions;
8
+ }
9
+ export interface ExtractorStats {
10
+ totalFiles: number;
11
+ totalNodes: number;
12
+ skippedFiles: number;
13
+ }
14
+ export interface ExtractorError {
15
+ file: string;
16
+ error: string;
17
+ }
18
+ export interface ExtractorResult {
19
+ fingerprint: ProjectFingerprint;
20
+ nodes: CodeNode[];
21
+ edges: CodeEdge[];
22
+ routes?: (RouteNode | BackendRouteNode)[];
23
+ stats: ExtractorStats;
24
+ errors: ExtractorError[];
25
+ }
26
+ export interface LanguageExtractor {
27
+ language: Language;
28
+ command: string;
29
+ args: string[];
30
+ /**
31
+ * Parse the raw stdout from the extractor process into ExtractorResult.
32
+ * The orchestrator calls this after the process exits successfully.
33
+ *
34
+ * Throwing here marks the extraction as failed.
35
+ */
36
+ parseResult: (stdout: string) => ExtractorResult;
37
+ }
@@ -0,0 +1,8 @@
1
+ // src/extractors/types.ts
2
+ //
3
+ // The Extractor Contract — defines the interface between the Node.js
4
+ // orchestrator and the native-language extractors (Python, Java, Go, Rust).
5
+ //
6
+ // Communication is JSON over stdin/stdout. See expansion-tracker/contract.html
7
+ // for the full spec.
8
+ export {};
@@ -6,5 +6,6 @@ export interface LookupMaps {
6
6
  storeNodes: CodeNode[];
7
7
  thirdPartyNodesByName: Map<string, CodeNode>;
8
8
  thirdPartyImportAliases: Map<string, Map<string, string>>;
9
+ localImportSymbols: Map<string, Map<string, string>>;
9
10
  }
10
11
  export declare function buildLookupMaps(codeNodes: CodeNode[]): LookupMaps;
@@ -5,6 +5,7 @@ export function buildLookupMaps(codeNodes) {
5
5
  const storeNodes = [];
6
6
  const thirdPartyNodesByName = new Map();
7
7
  const thirdPartyImportAliases = new Map();
8
+ const localImportSymbols = new Map();
8
9
  for (const node of codeNodes) {
9
10
  if (node.type === "THIRD_PARTY") {
10
11
  thirdPartyNodesByName.set(node.name, node);
@@ -28,5 +29,5 @@ export function buildLookupMaps(codeNodes) {
28
29
  storeNodes.push(node);
29
30
  }
30
31
  }
31
- return { nodesByName, nodesByFile, fileNodesByPath, storeNodes, thirdPartyNodesByName, thirdPartyImportAliases };
32
+ return { nodesByName, nodesByFile, fileNodesByPath, storeNodes, thirdPartyNodesByName, thirdPartyImportAliases, localImportSymbols };
32
33
  }