descry-codegraph 0.3.0__tar.gz → 0.3.2__tar.gz

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 (134) hide show
  1. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/CHANGELOG.md +126 -0
  2. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/CLAUDE.md +1 -1
  3. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/PKG-INFO +2 -2
  4. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/README.md +1 -1
  5. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/pyproject.toml +1 -1
  6. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/__init__.py +1 -1
  7. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/_graph.py +19 -0
  8. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/cli.py +7 -2
  9. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/embeddings.py +10 -5
  10. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/generate.py +7 -20
  11. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/git_history.py +40 -34
  12. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/handlers.py +201 -179
  13. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/mcp_server.py +4 -4
  14. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/query.py +578 -740
  15. descry_codegraph-0.3.2/src/descry/scip/adapters/_empty_environment.json +1 -0
  16. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/adapters/python.py +12 -1
  17. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/cache.py +7 -3
  18. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/web/server.py +26 -9
  19. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/web/web/index.html +45 -17
  20. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_cli.py +1 -1
  21. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_embeddings.py +17 -0
  22. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_generate.py +19 -0
  23. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_git_history.py +61 -2
  24. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_handlers.py +160 -8
  25. descry_codegraph-0.3.2/tests/test_interfaces.py +296 -0
  26. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_mcp.py +1 -1
  27. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_query.py +704 -64
  28. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_scip_cache.py +38 -0
  29. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_web_server.py +101 -25
  30. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/.github/assets/01-landing.png +0 -0
  31. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/.github/workflows/ci.yml +0 -0
  32. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/.github/workflows/release.yml +0 -0
  33. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/.gitignore +0 -0
  34. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/LICENSE +0 -0
  35. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/SECURITY.md +0 -0
  36. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/THIRD-PARTY-NOTICES.md +0 -0
  37. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/claude-skill/README.md +0 -0
  38. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/claude-skill/descry/SKILL.md +0 -0
  39. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/claude-skill/descry/references/cli-reference.md +0 -0
  40. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/claude-skill/descry/references/mcp-tools.md +0 -0
  41. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/claude-skill/descry/references/recipes.md +0 -0
  42. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/justfile +0 -0
  43. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/pi-extension/README.md +0 -0
  44. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/pi-extension/descry-cli.py +0 -0
  45. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/pi-extension/descry-tools.ts +0 -0
  46. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/_comments.py +0 -0
  47. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/_env.py +0 -0
  48. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/_files.py +0 -0
  49. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/_stdlib.py +0 -0
  50. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/clang_parser.py +0 -0
  51. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/cross_lang.py +0 -0
  52. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/dart_parser.py +0 -0
  53. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/dotnet_parser.py +0 -0
  54. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/go_parser.py +0 -0
  55. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/java_parser.py +0 -0
  56. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/php_parser.py +0 -0
  57. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/ruby_parser.py +0 -0
  58. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/__init__.py +0 -0
  59. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/adapter.py +0 -0
  60. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/adapters/__init__.py +0 -0
  61. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/adapters/_java_compat.init.gradle +0 -0
  62. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/adapters/clang.py +0 -0
  63. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/adapters/dart.py +0 -0
  64. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/adapters/dotnet.py +0 -0
  65. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/adapters/go.py +0 -0
  66. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/adapters/java.py +0 -0
  67. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/adapters/php.py +0 -0
  68. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/adapters/ruby.py +0 -0
  69. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/adapters/rust.py +0 -0
  70. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/adapters/typescript.py +0 -0
  71. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/parser.py +0 -0
  72. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/pb2.py +0 -0
  73. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/scip/support.py +0 -0
  74. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/stdlib/c.json +0 -0
  75. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/stdlib/cpp.json +0 -0
  76. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/stdlib/dart.json +0 -0
  77. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/stdlib/dotnet.json +0 -0
  78. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/stdlib/go.json +0 -0
  79. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/stdlib/java.json +0 -0
  80. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/stdlib/node.json +0 -0
  81. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/stdlib/php.json +0 -0
  82. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/stdlib/python.json +0 -0
  83. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/stdlib/ruby.json +0 -0
  84. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/stdlib/rust.json +0 -0
  85. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/stdlib/typescript.json +0 -0
  86. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/web/__init__.py +0 -0
  87. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/src/descry/web/web/style.css +0 -0
  88. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/__init__.py +0 -0
  89. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/conftest.py +0 -0
  90. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/eval/README.md +0 -0
  91. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/eval/__init__.py +0 -0
  92. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/eval/compare.py +0 -0
  93. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/eval/corpus.py +0 -0
  94. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/eval/metrics.py +0 -0
  95. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/eval/registry.py +0 -0
  96. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/eval/results/results-descry.json +0 -0
  97. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/eval/results/results-ripgrep.json +0 -0
  98. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/fixtures/small_graph.json +0 -0
  99. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_backtracking.py +0 -0
  100. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_clang_parser.py +0 -0
  101. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_config.py +0 -0
  102. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_cross_lang.py +0 -0
  103. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_dart_parser.py +0 -0
  104. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_dotnet_parser.py +0 -0
  105. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_go_parser.py +0 -0
  106. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_hardening.py +0 -0
  107. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_java_parser.py +0 -0
  108. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_non_regular_files.py +0 -0
  109. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_php_parser.py +0 -0
  110. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_ruby_parser.py +0 -0
  111. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_scip.py +0 -0
  112. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_scip_clang.py +0 -0
  113. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_scip_dart.py +0 -0
  114. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_scip_dotnet.py +0 -0
  115. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_scip_go.py +0 -0
  116. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_scip_java.py +0 -0
  117. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_scip_php.py +0 -0
  118. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_scip_ruby.py +0 -0
  119. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_stdlib.py +0 -0
  120. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tests/test_written_through.py +0 -0
  121. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/README.md +0 -0
  122. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/common.py +0 -0
  123. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/gen_c.py +0 -0
  124. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/gen_cpp.py +0 -0
  125. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/gen_dart.py +0 -0
  126. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/gen_dotnet.py +0 -0
  127. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/gen_go.py +0 -0
  128. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/gen_java.py +0 -0
  129. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/gen_node.py +0 -0
  130. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/gen_php.py +0 -0
  131. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/gen_python.py +0 -0
  132. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/gen_ruby.py +0 -0
  133. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/gen_rust.py +0 -0
  134. {descry_codegraph-0.3.0 → descry_codegraph-0.3.2}/tools/stdlib/gen_typescript.py +0 -0
@@ -6,6 +6,132 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.2] — 2026-10-23
10
+
11
+ ### Fixed
12
+
13
+ - A caller that reaches a symbol only as one of an unresolved call's
14
+ candidates is counted apart from its callers. `in_degree` ("N callers")
15
+ counts the callers whose calls are resolved to the symbol, and is
16
+ counted again as a graph loads, so a graph from 0.3.1 needs no reindex.
17
+ Every other count gives the others as "(+N possible)": brief `context`
18
+ (callers and callees), `callers`, `callees`, and the web page's headers
19
+ (`possible` in the web API). Lists mark each one; `changes` does so in
20
+ text, in its structured form (`candidates`, `callers_possible`) and on
21
+ the page.
22
+ - A function no longer counts as a certain caller of a symbol it defines
23
+ when its call to it is unresolved.
24
+ - A node ID names that node alone: `callers`, `callees`, `quick`, `flow`
25
+ and `path` given one no longer also match other definitions filed under
26
+ the same owner and name (a Ruby `self.x` beside `x`).
27
+ - `context`'s usage examples are calls of that symbol, those resolved to
28
+ it first, then those it is one of the candidates of, marked; they were
29
+ any unresolved call ending in its name.
30
+ - `callers` of a name lists the calls the resolver left unresolved with no
31
+ candidates apart from its callers, marked "(unresolved; same name)" and
32
+ counted as "+N unresolved" (`unresolved` in the web API). Some are calls
33
+ of the project's definition the resolver cannot follow (a Rust re-export,
34
+ a TypeScript static member), some are calls of something else with that
35
+ name (`loader.list_templates()` on a library's loader). Neither is
36
+ counted as a caller. An unresolved call with candidates reaches only
37
+ those. Callers asked for by node ID are those resolved to it or narrowed
38
+ to it.
39
+ - A list cut short (`callers`, `callees`, `context`'s callers, `changes`,
40
+ backward `flow` in text and as a tree) keeps the callers resolved to the
41
+ symbol first, and says how many it leaves out and how many of those only
42
+ may be; a `flow` tree node gives them as `more` and `more_possible`, and
43
+ the web API's `callers` and `callees` as `more`, shown on the page.
44
+ - `flow`, in text and as a tree, places each symbol at the first level
45
+ that reaches it and marks it "(at level N)" where it is met again
46
+ (`seen`, that level, in the tree); a direct callee or caller reached
47
+ first through another one was dropped from its level, with everything
48
+ below it. `context`, at every depth, lists all of the symbol's own
49
+ callees and cuts each level below to the per-level limit with a note; it
50
+ opens a callee (inlines it and lists its own callees) at the first level
51
+ that reaches it while the token budget covers it, lists every other
52
+ callee by its signature or "(at level N)", and says when the budget, the
53
+ node limit or the timeout cut it short. Depth 1 ignored `max_tokens`. A
54
+ `flow` that stops early says so: "(timeout)" or "(node limit)" in text,
55
+ `cut` in the tree, a note on the page.
56
+ - A `flow` of `depth` N shows N levels of calls below the start; `depth=1`
57
+ showed the start alone. `[query] max_depth` is that many levels too, for
58
+ `flow` and for `context`'s callee expansion. A depth past it is traced to
59
+ it and says so; the web API returns the depth traced, and `/api/health`
60
+ gives the ceiling the page's depth field is held to.
61
+ - A callee the resolver left unresolved is no longer shown or counted as
62
+ certain or as external. `context`, at every depth, names it as one of
63
+ its candidates, or "(unresolved)" where it found none. `callees` marks
64
+ it the same way and counts it apart ("+N unresolved"). A callee is what
65
+ a call is written as: calls written apart (`a.add`, `b.add`) are two
66
+ callees, and calls written alike may reach any definition one of them
67
+ is narrowed to. Forward `flow` follows a possible callee to each
68
+ definition it may reach, marked, and `context` at a depth past 1 says
69
+ how many callees each level leaves out.
70
+ - `callees` says how many callees its `limit` leaves out, and of a name
71
+ several definitions share, which one it answers for (`others` in the web
72
+ API).
73
+ - A `limit`, a `flow` or `context` `depth`, a `context` `max_tokens` or
74
+ `callee_budget`, or a `path` `max_depth` below 1 is refused by every
75
+ tool; `limit=0` answered as if there were no
76
+ callers, matches, changes or history.
77
+ - The web page's changes view no longer throws for a changed symbol with
78
+ no callers.
79
+ - `context --max-output-tokens` cuts the parts the answer is made of, not
80
+ its text: expanded callees, then usage examples past 2, related tests
81
+ and configuration, callers past 5 (still counting possible callers
82
+ apart), the symbol's callees past 5 (planned again for those five, so
83
+ the note counts by kind only callees drawn nowhere in the answer and no
84
+ "(at level N)" mark points at a callee cut away), and the source past 30
85
+ of the symbol's lines, saying what each step left out; a section dropped
86
+ whole keeps its heading with "(omitted, max_output_tokens limit)".
87
+ It read the rendered text back, so code with a heading-like or a fence
88
+ line in it was cut wrongly or not at all, opened callees kept their
89
+ headings without their code, and counts were wrong.
90
+ - `flow` text indents the code it inlines as deep as its fence. The code
91
+ was printed from the first column inside an indented fence, so it broke
92
+ the tree, and below the first level the fence read as an indented code
93
+ block.
94
+ - A command that loads no embedding model no longer imports
95
+ sentence-transformers, and with it torch, as the service starts: where
96
+ they are installed every CLI command spent seconds on it.
97
+ - `churn` ranks symbols, and co-change pairs of symbols, with the same
98
+ number of commits by node ID, so the same history gives the same list.
99
+ Their order among ties changed from run to run, and so, at the limit,
100
+ did which of them were listed.
101
+
102
+ ## [0.3.1] — 2026-10-22
103
+
104
+ ### Fixed
105
+
106
+ - The callers a change lists (`changes`), and those `context` lists and
107
+ counts and an upstream `flow` follows, are the callers of that symbol,
108
+ not of every symbol of its name. `context` marks a caller that reaches
109
+ the symbol only as one of an unresolved call's candidates, as `callers`
110
+ does.
111
+ - A symbol's caller count (`in_degree`, shown as "N callers") counts the
112
+ symbols that call or construct it, not every edge into it: the file
113
+ defining it was counted as a caller.
114
+ - A Python signature is written as the source writes it, with defaults,
115
+ `*`, `**`, keyword-only parameters and `async`, and with no `-> None`
116
+ where no return type is annotated. An unannotated function was recorded
117
+ as returning `None`.
118
+ - The web UI's Language filter lists the graph's languages, which the
119
+ graph status now gives (`/api/status`, `/api/health`), instead of four
120
+ fixed ones.
121
+ - A search during the startup pre-warm, or during the embeddings load
122
+ after an index, waits for that load (up to `embedding_timeout`) instead
123
+ of reporting that embeddings are loading. A streamed index reports the
124
+ embeddings line however long the load takes.
125
+ - scip-python runs without `pip` on PATH and outside a git repository: it
126
+ is given an empty environment and a project version.
127
+ - A SCIP indexer's failure is reported by its error, without the stack
128
+ frames that pushed it out of the message.
129
+ - `flatten` takes a class name as `context` does, not only a node ID.
130
+ - `impls` says trait implementations are recorded for Rust only, instead
131
+ of advising a reindex.
132
+ - An unresolved call with one candidate reads "unresolved; the only
133
+ candidate", not "one of 1 candidates".
134
+
9
135
  ## [0.3.0] — 2026-10-22
10
136
 
11
137
  Call resolution is rebuilt for every supported language: a call is
@@ -23,7 +23,7 @@ just test tests/test_handlers.py::test_index # one test
23
23
  just test -k "cross_lang and not slow" # by expression
24
24
  ```
25
25
 
26
- Python 3.11+ required (enforced by `pyproject.toml`). Pre-commit gate: `just lint && just fmt && just test` all pass (2722 tests currently — a drop is a regression signal).
26
+ Python 3.11+ required (enforced by `pyproject.toml`). Pre-commit gate: `just lint && just fmt && just test` all pass (2810 tests currently — a drop is a regression signal).
27
27
 
28
28
  Invoke the tool while developing:
29
29
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: descry-codegraph
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Codebase knowledge graph: polyglot indexer, semantic search, call-graph analysis
5
5
  Project-URL: Homepage, https://github.com/06chaynes/descry
6
6
  Project-URL: Repository, https://github.com/06chaynes/descry
@@ -251,7 +251,7 @@ Descry provides 19 tools, available through all interfaces:
251
251
  | `path` | Find shortest call path between two symbols |
252
252
  | `structure` | Show file structure — imports, classes, functions |
253
253
  | `flatten` | Show effective API of a class including inherited methods |
254
- | `impls` | Find all implementations of a trait/interface method |
254
+ | `impls` | Find all implementations of a Rust trait method |
255
255
  | `cross-lang` | Trace frontend API calls to backend handlers via OpenAPI |
256
256
  | `churn` | Find code churn hotspots (symbols, files, or co-change pairs) |
257
257
  | `evolution` | Track how a symbol has changed over time |
@@ -186,7 +186,7 @@ Descry provides 19 tools, available through all interfaces:
186
186
  | `path` | Find shortest call path between two symbols |
187
187
  | `structure` | Show file structure — imports, classes, functions |
188
188
  | `flatten` | Show effective API of a class including inherited methods |
189
- | `impls` | Find all implementations of a trait/interface method |
189
+ | `impls` | Find all implementations of a Rust trait method |
190
190
  | `cross-lang` | Trace frontend API calls to backend handlers via OpenAPI |
191
191
  | `churn` | Find code churn hotspots (symbols, files, or co-change pairs) |
192
192
  | `evolution` | Track how a symbol has changed over time |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "descry-codegraph"
3
- version = "0.3.0"
3
+ version = "0.3.2"
4
4
  description = "Codebase knowledge graph: polyglot indexer, semantic search, call-graph analysis"
5
5
  requires-python = ">=3.11"
6
6
  authors = [{name = "Christian Haynes"}]
@@ -1,4 +1,4 @@
1
1
  """Descry — Codebase knowledge graph: polyglot indexer, semantic search, call-graph analysis."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.2"
4
4
  __all__ = ["__version__"]
@@ -11,6 +11,11 @@ from pathlib import Path
11
11
 
12
12
  CURRENT_SCHEMA = 2
13
13
 
14
+ # The edges by which one symbol calls or constructs another: its callers
15
+ CALL_RELATIONS = frozenset(
16
+ {"CALLS", "CALLS_RESOLVED", "INSTANTIATES", "INSTANTIATES_RESOLVED"}
17
+ )
18
+
14
19
 
15
20
  class GraphSchemaError(Exception):
16
21
  """Raised when a graph file is not a graph this version can read.
@@ -107,4 +112,18 @@ def load_graph_with_schema(path: Path | str) -> dict:
107
112
  f"run 'descry index' to rebuild."
108
113
  )
109
114
  _check_shape(data)
115
+ count_callers(data["nodes"], data["edges"])
110
116
  return data
117
+
118
+
119
+ def count_callers(nodes: list, edges: list) -> None:
120
+ """Set each node's `in_degree` to the number of its callers: the
121
+ symbols whose calls are resolved to it, not those an unresolved call
122
+ only may be. A graph is counted again as it loads, so one written by an
123
+ older descry counts them the same way."""
124
+ callers: dict[str, set] = {}
125
+ for edge in edges:
126
+ if edge["relation"] in CALL_RELATIONS:
127
+ callers.setdefault(edge["target"], set()).add(edge["source"])
128
+ for node in nodes:
129
+ node["metadata"]["in_degree"] = len(callers.get(node["id"], ()))
@@ -302,7 +302,12 @@ def main():
302
302
  choices=["forward", "backward"],
303
303
  help="Trace direction (default: forward)",
304
304
  )
305
- sub.add_argument("--depth", type=int, default=3, help="Trace depth (default: 3)")
305
+ sub.add_argument(
306
+ "--depth",
307
+ type=int,
308
+ default=3,
309
+ help="Levels of calls below the start (default: 3)",
310
+ )
306
311
  sub.add_argument("--target", default=None, help="Target symbol to reach")
307
312
  sub.add_argument(
308
313
  "--inline-threshold",
@@ -368,7 +373,7 @@ def main():
368
373
 
369
374
  # impls
370
375
  sub = subparsers.add_parser(
371
- "impls", help="Find all implementations of a trait method"
376
+ "impls", help="Find all implementations of a Rust trait method"
372
377
  )
373
378
  sub.add_argument("method", help="Method name")
374
379
  sub.add_argument("--trait-name", default=None, help="Filter by trait name")
@@ -12,6 +12,7 @@ Usage:
12
12
 
13
13
  import contextlib
14
14
  import hashlib
15
+ import importlib.util
15
16
  import json
16
17
  import logging
17
18
  import os
@@ -347,13 +348,14 @@ def _resolve_cache_dir(graph_path: Path, cache_dir: str | None = None) -> Path:
347
348
  # Try to import embedding dependencies
348
349
  try:
349
350
  import numpy as np
350
- from sentence_transformers import SentenceTransformer
351
-
352
- EMBEDDINGS_AVAILABLE = True
353
351
  except ImportError:
354
- EMBEDDINGS_AVAILABLE = False
355
352
  np = None
356
- SentenceTransformer = None
353
+ # Imported by _load_sentence_transformer: it brings in torch, which takes
354
+ # seconds a command that loads no model should not spend.
355
+ SentenceTransformer = None
356
+ EMBEDDINGS_AVAILABLE = (
357
+ np is not None and importlib.util.find_spec("sentence_transformers") is not None
358
+ )
357
359
 
358
360
 
359
361
  def embeddings_available() -> bool:
@@ -376,6 +378,9 @@ def _load_sentence_transformer(model_name: str, revision: str | None = None):
376
378
  model_name: Registry alias, HuggingFace repo id, or local path.
377
379
  revision: Overrides the registry's pinned revision.
378
380
  """
381
+ global SentenceTransformer
382
+ if SentenceTransformer is None:
383
+ from sentence_transformers import SentenceTransformer
379
384
  spec = resolve_model_spec(model_name)
380
385
  target = spec.repo_id if spec else model_name
381
386
  if revision is None and spec:
@@ -8388,15 +8388,6 @@ class PythonParser(BaseParser):
8388
8388
  return f"[{elts}]"
8389
8389
  return "Complex"
8390
8390
 
8391
- def _get_args_info(self, args_node):
8392
- args_list = []
8393
- for arg in args_node.args:
8394
- arg_str = arg.arg
8395
- if arg.annotation:
8396
- arg_str += f": {self._get_type_annotation(arg.annotation)}"
8397
- args_list.append(arg_str)
8398
- return args_list
8399
-
8400
8391
  def _get_param_types(self, args_node):
8401
8392
  """Extract parameter types as a list for metadata storage."""
8402
8393
  param_types = []
@@ -8492,12 +8483,14 @@ class PythonParser(BaseParser):
8492
8483
  is_method = parent_id in self.class_ids
8493
8484
  node_type = "Method" if is_method else "Function"
8494
8485
 
8495
- args = self._get_args_info(node.args)
8496
8486
  param_types = self._get_param_types(node.args)
8497
8487
  return_type = (
8498
- self._get_type_annotation(node.returns) if node.returns else "None"
8488
+ self._get_type_annotation(node.returns) if node.returns else ""
8499
8489
  )
8500
- signature = f"def {node.name}({', '.join(args)}) -> {return_type}"
8490
+ keyword = "async def" if isinstance(node, ast.AsyncFunctionDef) else "def"
8491
+ signature = f"{keyword} {node.name}({ast.unparse(node.args)})"
8492
+ if node.returns:
8493
+ signature += f" -> {ast.unparse(node.returns)}"
8501
8494
 
8502
8495
  self.builder.add_node(
8503
8496
  func_id,
@@ -25440,16 +25433,10 @@ class CodeGraphBuilder:
25440
25433
  output_path: Path to write the graph JSON
25441
25434
  scip_index: Optional ScipIndex for type-aware resolution
25442
25435
  """
25443
- from descry._graph import CURRENT_SCHEMA
25436
+ from descry._graph import CURRENT_SCHEMA, count_callers
25444
25437
 
25445
25438
  self.resolve_references(scip_index=scip_index)
25446
- in_degree = {}
25447
- for edge in self.edges:
25448
- t = edge["target"]
25449
- if t:
25450
- in_degree[t] = in_degree.get(t, 0) + 1
25451
- for node in self.nodes:
25452
- node["metadata"]["in_degree"] = in_degree.get(node["id"], 0)
25439
+ count_callers(self.nodes, self.edges)
25453
25440
  # Atomic write: stream to a sibling tmp file then os.replace. A
25454
25441
  # concurrent reader (MCP prewarm, web `/api/health`, another CLI
25455
25442
  # invocation) that stats the destination mid-write would otherwise
@@ -758,7 +758,7 @@ class GitHistoryAnalyzer:
758
758
 
759
759
  # Sort by commit count
760
760
  sorted_symbols = sorted(
761
- symbol_commits.items(), key=lambda x: len(x[1]), reverse=True
761
+ symbol_commits.items(), key=lambda x: (-len(x[1]), x[0])
762
762
  )[:limit]
763
763
 
764
764
  lines = []
@@ -923,7 +923,7 @@ class GitHistoryAnalyzer:
923
923
 
924
924
  # SYMBOLS MODE
925
925
  sorted_symbols = sorted(
926
- symbol_commits.items(), key=lambda x: len(x[1]), reverse=True
926
+ symbol_commits.items(), key=lambda x: (-len(x[1]), x[0])
927
927
  )[:limit]
928
928
 
929
929
  symbols = []
@@ -983,7 +983,7 @@ class GitHistoryAnalyzer:
983
983
  frequent_pairs = [
984
984
  (pair, count) for pair, count in pair_counts.items() if count >= 2
985
985
  ]
986
- frequent_pairs.sort(key=lambda x: x[1], reverse=True)
986
+ frequent_pairs.sort(key=lambda x: (-x[1], x[0]))
987
987
 
988
988
  for (sym_a, sym_b), count in frequent_pairs[:limit]:
989
989
  _, name_a, loc_a = self._get_node_display(sym_a)
@@ -1081,7 +1081,7 @@ class GitHistoryAnalyzer:
1081
1081
  frequent_pairs = [
1082
1082
  (pair, count) for pair, count in pair_counts.items() if count >= 2
1083
1083
  ]
1084
- frequent_pairs.sort(key=lambda x: x[1], reverse=True)
1084
+ frequent_pairs.sort(key=lambda x: (-x[1], x[0]))
1085
1085
  frequent_pairs = frequent_pairs[:limit]
1086
1086
 
1087
1087
  if frequent_pairs:
@@ -1660,26 +1660,43 @@ class GitHistoryAnalyzer:
1660
1660
  f"(+{stats['added']}/-{stats['removed']})"
1661
1661
  )
1662
1662
 
1663
- # Show callers if requested
1664
1663
  if show_callers and self.querier:
1665
- sym_name = name.split(".")[-1] # Get base name
1666
- callers = self.querier.get_callers(sym_name)
1664
+ callers, total, possible = self._symbol_callers(node_id)
1667
1665
  if callers:
1668
- caller_names = []
1669
- for caller_id in callers[:5]:
1670
- c_node = self.querier.get_node_info(caller_id)
1671
- if c_node:
1672
- caller_names.append(c_node["metadata"].get("name", "?"))
1673
- else:
1674
- # Extract name from node ID
1675
- parts = caller_id.split("::")
1676
- caller_names.append(parts[-1] if parts else "?")
1677
-
1678
- more = f", +{len(callers) - 5} more" if len(callers) > 5 else ""
1679
- lines.append(f" Callers: {', '.join(caller_names)}{more}")
1666
+ names = ", ".join(
1667
+ c["name"] + self.querier.one_of(c["candidates"])
1668
+ for c in callers
1669
+ )
1670
+ hidden = possible - sum(bool(c["candidates"]) for c in callers)
1671
+ hidden_certain = total - len(callers) - hidden
1672
+ more = (
1673
+ f"; {self.querier.more_callers(hidden_certain, hidden)}"
1674
+ if total > len(callers)
1675
+ else ""
1676
+ )
1677
+ lines.append(f" Callers: {names}{more}")
1680
1678
 
1681
1679
  return "\n".join(lines)
1682
1680
 
1681
+ def _symbol_callers(self, node_id):
1682
+ """The first five callers of the symbol ``node_id``, certain ones
1683
+ first, each ``{"id", "name", "candidates"}`` where ``candidates`` is
1684
+ the number an unresolved call reaching it only may be one of; the
1685
+ number of its callers; and how many of those only may be."""
1686
+ caller_ids, ambiguous = self.querier.ordered_callers(node_id)
1687
+ callers = []
1688
+ for caller_id in caller_ids[:5]:
1689
+ c_node = self.querier.get_node_info(caller_id)
1690
+ name = (
1691
+ c_node["metadata"].get("name", "?")
1692
+ if c_node
1693
+ else caller_id.split("::")[-1]
1694
+ )
1695
+ callers.append(
1696
+ {"id": caller_id, "name": name, "candidates": ambiguous.get(caller_id)}
1697
+ )
1698
+ return callers, len(caller_ids), len(ambiguous)
1699
+
1683
1700
  def get_changes_structured(
1684
1701
  self,
1685
1702
  commit_range: str | None = None,
@@ -1742,22 +1759,11 @@ class GitHistoryAnalyzer:
1742
1759
  }
1743
1760
 
1744
1761
  if show_callers and self.querier:
1745
- sym_name = name.split(".")[-1]
1746
- caller_ids = self.querier.get_callers(sym_name)
1747
- if caller_ids:
1748
- callers = []
1749
- for caller_id in caller_ids[:5]:
1750
- c_node = self.querier.get_node_info(caller_id)
1751
- c_name = (
1752
- c_node["metadata"].get("name", "?")
1753
- if c_node
1754
- else caller_id.split("::")[-1]
1755
- if "::" in caller_id
1756
- else "?"
1757
- )
1758
- callers.append({"id": caller_id, "name": c_name})
1762
+ callers, total, possible = self._symbol_callers(node_id)
1763
+ if callers:
1759
1764
  entry["callers"] = callers
1760
- entry["callers_total"] = len(caller_ids)
1765
+ entry["callers_total"] = total
1766
+ entry["callers_possible"] = possible
1761
1767
 
1762
1768
  symbols.append(entry)
1763
1769