devlensio 1.0.1 → 1.0.3

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.
@@ -46,6 +46,7 @@ export interface PipelineResult {
46
46
  isGithubRepo: boolean;
47
47
  gitInfo: GitInfo;
48
48
  }
49
+ export declare function worktreeCommitHash(nodes: CodeNode[]): string;
49
50
  export declare function routesToCodeNodes(routes: (RouteNode | BackendRouteNode)[], repoPath: string): CodeNode[];
50
51
  export declare function analyzePipeline(repoPath: string, isGithubRepo: boolean, options?: PipelineOptions): Promise<PipelineResult>;
51
52
  export declare function refilterPipeline(stored: PipelineResult, thresholds: FilterThresholds): Pick<PipelineResult, "nodes" | "edges" | "stats">;
@@ -15,6 +15,12 @@ function generateGraphId(repoPath, isGithubRepo) {
15
15
  .digest("hex")
16
16
  .slice(0, 16);
17
17
  }
18
+ // Stable id for repos without git: derived from node ids + code hashes, so the
19
+ // same content overwrites one snapshot in place instead of accumulating timestamps
20
+ export function worktreeCommitHash(nodes) {
21
+ const parts = nodes.map((n) => `${n.id}:${n.codeHash ?? ""}`).sort();
22
+ return "worktree-" + createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 12);
23
+ }
18
24
  // Gets current git state of the repo
19
25
  // Falls back gracefully if git is not initialized
20
26
  function getGitInfo(repoPath) {
@@ -28,9 +34,11 @@ function getGitInfo(repoPath) {
28
34
  return { commitHash, branch, message, hasGit: true };
29
35
  }
30
36
  catch {
31
- // No git, or no commits yet — use timestamp as version key
37
+ // No git, or no commits yet. Placeholder resolved to a content-derived
38
+ // id after extraction (see worktreeCommitHash) so unchanged repos reuse
39
+ // one snapshot instead of growing a new timestamped one per analyze
32
40
  return {
33
- commitHash: Date.now().toString(),
41
+ commitHash: "worktree-pending",
34
42
  branch: "unknown",
35
43
  message: "no git history",
36
44
  hasGit: false,
@@ -221,6 +229,10 @@ export async function analyzePipeline(repoPath, isGithubRepo, options) {
221
229
  }
222
230
  const allNodes = extractorResult.nodes;
223
231
  const allEdges = extractorResult.edges;
232
+ if (!gitInfo.hasGit) {
233
+ gitInfo.commitHash = worktreeCommitHash(allNodes);
234
+ gitInfo.message = "worktree snapshot";
235
+ }
224
236
  // Step 5: Score and filter
225
237
  console.log("\n[5/5] Scoring and filtering...");
226
238
  const scoringResult = scoreAndFilter(allNodes, allEdges, options?.thresholds);
@@ -21,6 +21,7 @@ export interface CommitSummary {
21
21
  edgeCount: number;
22
22
  hasGit: boolean;
23
23
  isSummarized?: boolean;
24
+ isIndexed?: boolean;
24
25
  }
25
26
  export interface GraphMeta {
26
27
  graphId: string;
@@ -101,6 +102,8 @@ export declare function deleteCommit(graphId: string, commitHash: string): boole
101
102
  export declare function diffCommits(graphId: string, fromHash: string, toHash: string): NodeDiff | undefined;
102
103
  export declare function markCommitSummarized(graphId: string, commitHash: string): void;
103
104
  export declare function isCommitSummarized(graphId: string, commitHash: string): boolean;
105
+ export declare function markCommitIndexed(graphId: string, commitHash: string): void;
106
+ export declare function isCommitIndexed(graphId: string, commitHash: string): boolean;
104
107
  export declare function findLastSummarizedAncestor(graphId: string, commitHash: string, repoPath: string): Promise<string | undefined>;
105
108
  export declare function saveNodeSummaries(graphId: string, commitHash: string, nodeUpdates: Map<string, {
106
109
  technicalSummary: string;
@@ -182,9 +182,14 @@ export function saveGraph(result, options) {
182
182
  commits: [],
183
183
  summarizedCommits: [],
184
184
  };
185
- // Replace if same commit already exists, else append
185
+ // Replace if same commit already exists, else append.
186
+ // Carry over flags the rebuilt summary does not know about (same hazard as
187
+ // isSummarized: buildCommitSummary starts from scratch)
186
188
  const existingCommit = meta.commits.findIndex((c) => c.commitHash === commitHash);
187
189
  if (existingCommit >= 0) {
190
+ const prev = meta.commits[existingCommit];
191
+ if (prev.isIndexed)
192
+ newSummary.isIndexed = true;
188
193
  meta.commits[existingCommit] = newSummary;
189
194
  }
190
195
  else {
@@ -500,6 +505,23 @@ export function isCommitSummarized(graphId, commitHash) {
500
505
  return false;
501
506
  return meta.summarizedCommits?.includes(commitHash);
502
507
  }
508
+ // Marks a commit as having a derived .search.json index
509
+ export function markCommitIndexed(graphId, commitHash) {
510
+ const meta = readMeta(graphId);
511
+ if (!meta)
512
+ return;
513
+ const commit = meta.commits.find((c) => c.commitHash === commitHash);
514
+ if (!commit || commit.isIndexed)
515
+ return;
516
+ commit.isIndexed = true;
517
+ writeMeta(meta);
518
+ }
519
+ export function isCommitIndexed(graphId, commitHash) {
520
+ const meta = readMeta(graphId);
521
+ if (!meta)
522
+ return false;
523
+ return meta.commits.find((c) => c.commitHash === commitHash)?.isIndexed === true;
524
+ }
503
525
  // Finds the most recent ancestor commit that has been summarized.
504
526
  // Uses simple-git to walk the commit history of the repo.
505
527
  // Returns undefined if no summarized ancestor exists (= full summarization needed).
@@ -601,6 +623,8 @@ export const fileStorage = {
601
623
  diffCommits,
602
624
  markCommitSummarized,
603
625
  isCommitSummarized,
626
+ markCommitIndexed,
627
+ isCommitIndexed,
604
628
  findLastSummarizedAncestor,
605
629
  saveNodeSummaries,
606
630
  getCheckpointPath,
@@ -11,6 +11,8 @@ export interface GraphStorage {
11
11
  diffCommits(graphId: string, fromHash: string, toHash: string): NodeDiff | undefined;
12
12
  markCommitSummarized(graphId: string, commitHash: string): void;
13
13
  isCommitSummarized(graphId: string, commitHash: string): boolean;
14
+ markCommitIndexed(graphId: string, commitHash: string): void;
15
+ isCommitIndexed(graphId: string, commitHash: string): boolean;
14
16
  findLastSummarizedAncestor(graphId: string, commitHash: string, repoPath: string): Promise<string | undefined>;
15
17
  saveNodeSummaries(graphId: string, commitHash: string, nodeUpdates: Map<string, {
16
18
  technicalSummary: string;
@@ -41,7 +41,7 @@ pub fn detect_calls(
41
41
  let mut edges = vec![];
42
42
 
43
43
  for pf in &repo.files {
44
- if pf.is_test_file {
44
+ if pf.is_pure_test_file {
45
45
  continue;
46
46
  }
47
47
  for item in &pf.items {
@@ -57,7 +57,7 @@ pub fn enrich_nodes(
57
57
  let mut model_table: std::collections::HashMap<String, String> =
58
58
  std::collections::HashMap::new();
59
59
  for pf in &repo.files {
60
- if pf.is_test_file {
60
+ if pf.is_pure_test_file {
61
61
  continue;
62
62
  }
63
63
  for item in &pf.items {
@@ -34,7 +34,7 @@ pub fn detect_imports(
34
34
  let mut seen: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
35
35
 
36
36
  for pf in &repo.files {
37
- if pf.is_test_file {
37
+ if pf.is_pure_test_file {
38
38
  // test files ARE leaf nodes, but they still import — emit edges
39
39
  // from the TEST node (same file id — parentFile covers it)
40
40
  }
@@ -37,7 +37,7 @@ pub fn detect_inheritance(
37
37
  let mut edges = vec![];
38
38
  let mut nodes = vec![];
39
39
  for pf in &repo.files {
40
- if pf.is_test_file {
40
+ if pf.is_pure_test_file {
41
41
  continue;
42
42
  }
43
43
  for item in &pf.items {
@@ -2,9 +2,11 @@
2
2
  //
3
3
  // Contract rules enforced here:
4
4
  // - FILE id `file::rel/path.rs`; file nodes parent themselves
5
- // - TEST files (any #[test] fn) = LEAF nodes: nodes=[] for them, test fns
6
- // ride in metadata.testCases only (the bug that shipped in Python's
7
- // parser — asserted in the bun suite)
5
+ // - PURE test files (tests/ integration files, or files whose every item is
6
+ // a #[test] fn) = LEAF TEST nodes; Rust's idiomatic inline #[cfg(test)]
7
+ // mod tests must NOT collapse a mixed production file — its non-test items
8
+ // emit normally and #[test] fns emit as FUNCTION nodes flagged
9
+ // metadata.isTestFunction (test bodies stay out of prod call edges there)
8
10
  // - rawCode + codeHash on every FUNCTION/METHOD/STRUCT/ENUM/TRAIT/IMPL_BLOCK
9
11
  // - deterministic ids (file-scoped; trait-impl method suffix for the
10
12
  // same-name-different-trait collision class)
@@ -17,7 +19,7 @@ use serde_json::{Map, Value};
17
19
  pub fn collect_file_nodes(files: &[ParsedFile]) -> Vec<CodeNode> {
18
20
  let mut out = vec![];
19
21
  for pf in files {
20
- if pf.is_test_file {
22
+ if pf.is_pure_test_file {
21
23
  let mut n = file_node(&pf.rel_path, pf.line_count, NODE_TEST);
22
24
  let meta = n
23
25
  .metadata
@@ -30,25 +32,43 @@ pub fn collect_file_nodes(files: &[ParsedFile]) -> Vec<CodeNode> {
30
32
  }
31
33
  out.push(n);
32
34
  } else {
33
- out.push(file_node(&pf.rel_path, pf.line_count, NODE_FILE));
35
+ let mut n = file_node(&pf.rel_path, pf.line_count, NODE_FILE);
36
+ // Mixed files keep a testCases index so consumers that never walk
37
+ // FUNCTION nodes still see the file's tests.
38
+ if !pf.test_fns.is_empty() {
39
+ let meta = n
40
+ .metadata
41
+ .entry("testCases".to_string())
42
+ .or_insert_with(|| Value::Array(vec![]));
43
+ if let Value::Array(arr) = meta {
44
+ for t in &pf.test_fns {
45
+ arr.push(Value::from(t.clone()));
46
+ }
47
+ }
48
+ }
49
+ out.push(n);
34
50
  }
35
51
  }
36
52
  out
37
53
  }
38
54
 
39
- /// Code nodes (FUNCTION/METHOD/STRUCT/ENUM/TRAIT/IMPL_BLOCK) — test fns are
40
- /// NOT emitted (leaf rule); they feed metadata.testCases only.
55
+ /// Code nodes. PURE test files stay leaves (their fns ride in testCases).
56
+ /// Mixed files emit ALL items; #[test] fns are flagged metadata.isTestFunction
57
+ /// instead of being dropped, so a test function is addressable without a new
58
+ /// node type.
41
59
  pub fn collect_code_nodes(files: &[ParsedFile]) -> Vec<CodeNode> {
42
60
  let mut out = vec![];
43
61
  for pf in files {
44
- if pf.is_test_file {
62
+ if pf.is_pure_test_file {
45
63
  continue;
46
64
  }
47
65
  for item in &pf.items {
66
+ let mut n = item_node(pf, item);
48
67
  if item.is_test {
49
- continue;
68
+ n.metadata
69
+ .insert("isTestFunction".to_string(), Value::from(true));
50
70
  }
51
- out.push(item_node(pf, item));
71
+ out.push(n);
52
72
  }
53
73
  }
54
74
  out
@@ -21,7 +21,7 @@ use std::collections::HashMap;
21
21
  pub fn table_to_model_map(repo: &ParsedRepo) -> HashMap<String, String> {
22
22
  let mut map = HashMap::new();
23
23
  for pf in &repo.files {
24
- if pf.is_test_file {
24
+ if pf.is_pure_test_file {
25
25
  continue;
26
26
  }
27
27
  for item in &pf.items {
@@ -89,7 +89,7 @@ pub fn detect_orm_edges(
89
89
  let table_to_schema_file = table_to_schema_file_map(repo);
90
90
 
91
91
  for pf in &repo.files {
92
- if pf.is_test_file {
92
+ if pf.is_pure_test_file {
93
93
  continue;
94
94
  }
95
95
  for item in &pf.items {
@@ -162,7 +162,12 @@ pub struct ParsedFile {
162
162
  pub items: Vec<ParsedItem>,
163
163
  pub tables: Vec<TableDecl>,
164
164
  pub test_fns: Vec<String>,
165
- pub is_test_file: bool,
165
+ /// True only when the file is EXCLUSIVELY tests (every top-level item is a
166
+ /// #[test] fn, or the file only holds inline #[cfg(test)] mods). Rust's
167
+ /// idiomatic inline `#[cfg(test)] mod tests` must NOT collapse a mixed
168
+ /// production file into a TEST leaf — only pure test files get leaf
169
+ /// treatment (mirrors the intent of go's _test.go rule).
170
+ pub is_pure_test_file: bool,
166
171
  pub line_count: i64,
167
172
  }
168
173
 
@@ -326,7 +331,7 @@ pub fn parse_file(rel_path: &str, source: &str) -> Result<ParsedFile, String> {
326
331
  items: vec![],
327
332
  tables: vec![],
328
333
  test_fns: vec![],
329
- is_test_file: false,
334
+ is_pure_test_file: false,
330
335
  line_count: line_index.line_count() as i64,
331
336
  };
332
337
 
@@ -342,7 +347,7 @@ pub fn parse_file(rel_path: &str, source: &str) -> Result<ParsedFile, String> {
342
347
  }
343
348
  pf.ast.items = ast_items;
344
349
 
345
- pf.is_test_file = !pf.test_fns.is_empty();
350
+ pf.is_pure_test_file = !pf.test_fns.is_empty() && pf.items.iter().all(|i| i.is_test);
346
351
  Ok(pf)
347
352
  }
348
353
 
@@ -96,7 +96,7 @@ pub fn detect_routes(
96
96
  let mut func_lit_vars: HashMap<(String, String), String> = HashMap::new();
97
97
 
98
98
  for pf in &repo.files {
99
- if pf.is_test_file {
99
+ if pf.is_pure_test_file {
100
100
  continue;
101
101
  }
102
102
  for item in &pf.items {
@@ -195,7 +195,7 @@ pub fn detect_routes(
195
195
  // ── actix-web / rocket: attribute macros ──
196
196
  if framework == "actix-web" || framework == "rocket" {
197
197
  for pf in &repo.files {
198
- if pf.is_test_file {
198
+ if pf.is_pure_test_file {
199
199
  continue;
200
200
  }
201
201
  for item in &pf.items {
@@ -22,7 +22,7 @@ pub fn detect_tests(
22
22
  ) -> Vec<CodeEdge> {
23
23
  let mut edges = vec![];
24
24
  for pf in &repo.files {
25
- if !pf.is_test_file {
25
+ if !pf.is_pure_test_file {
26
26
  continue;
27
27
  }
28
28
  let from = format!("file::{}", pf.rel_path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlensio",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Codebase intelligence engine for TypeScript/JavaScript/React/Next.js, Python, Java, Go, and Rust repositories.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",