descry-codegraph 0.3.1__tar.gz → 0.3.3__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 (135) hide show
  1. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/CHANGELOG.md +106 -0
  2. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/CLAUDE.md +1 -1
  3. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/PKG-INFO +3 -3
  4. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/README.md +2 -2
  5. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/pyproject.toml +1 -1
  6. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/__init__.py +1 -1
  7. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/_graph.py +14 -0
  8. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/cli.py +6 -1
  9. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/embeddings.py +10 -5
  10. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/generate.py +21 -14
  11. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/git_history.py +40 -33
  12. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/handlers.py +83 -36
  13. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/mcp_server.py +1 -1
  14. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/query.py +562 -718
  15. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/web/server.py +26 -9
  16. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/web/web/index.html +32 -8
  17. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_cli.py +1 -1
  18. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_embeddings.py +17 -0
  19. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_generate.py +48 -0
  20. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_git_history.py +16 -2
  21. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_handlers.py +116 -5
  22. descry_codegraph-0.3.3/tests/test_interfaces.py +296 -0
  23. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_mcp.py +1 -1
  24. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_query.py +638 -63
  25. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_web_server.py +59 -6
  26. descry_codegraph-0.3.1/tests/test_interfaces.py +0 -178
  27. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/.github/assets/01-landing.png +0 -0
  28. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/.github/workflows/ci.yml +0 -0
  29. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/.github/workflows/release.yml +0 -0
  30. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/.gitignore +0 -0
  31. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/LICENSE +0 -0
  32. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/SECURITY.md +0 -0
  33. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/THIRD-PARTY-NOTICES.md +0 -0
  34. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/claude-skill/README.md +0 -0
  35. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/claude-skill/descry/SKILL.md +0 -0
  36. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/claude-skill/descry/references/cli-reference.md +0 -0
  37. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/claude-skill/descry/references/mcp-tools.md +0 -0
  38. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/claude-skill/descry/references/recipes.md +0 -0
  39. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/justfile +0 -0
  40. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/pi-extension/README.md +0 -0
  41. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/pi-extension/descry-cli.py +0 -0
  42. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/pi-extension/descry-tools.ts +0 -0
  43. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/_comments.py +0 -0
  44. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/_env.py +0 -0
  45. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/_files.py +0 -0
  46. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/_stdlib.py +0 -0
  47. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/clang_parser.py +0 -0
  48. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/cross_lang.py +0 -0
  49. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/dart_parser.py +0 -0
  50. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/dotnet_parser.py +0 -0
  51. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/go_parser.py +0 -0
  52. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/java_parser.py +0 -0
  53. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/php_parser.py +0 -0
  54. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/ruby_parser.py +0 -0
  55. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/__init__.py +0 -0
  56. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapter.py +0 -0
  57. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapters/__init__.py +0 -0
  58. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapters/_empty_environment.json +0 -0
  59. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapters/_java_compat.init.gradle +0 -0
  60. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapters/clang.py +0 -0
  61. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapters/dart.py +0 -0
  62. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapters/dotnet.py +0 -0
  63. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapters/go.py +0 -0
  64. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapters/java.py +0 -0
  65. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapters/php.py +0 -0
  66. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapters/python.py +0 -0
  67. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapters/ruby.py +0 -0
  68. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapters/rust.py +0 -0
  69. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/adapters/typescript.py +0 -0
  70. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/cache.py +0 -0
  71. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/parser.py +0 -0
  72. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/pb2.py +0 -0
  73. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/scip/support.py +0 -0
  74. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/stdlib/c.json +0 -0
  75. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/stdlib/cpp.json +0 -0
  76. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/stdlib/dart.json +0 -0
  77. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/stdlib/dotnet.json +0 -0
  78. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/stdlib/go.json +0 -0
  79. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/stdlib/java.json +0 -0
  80. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/stdlib/node.json +0 -0
  81. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/stdlib/php.json +0 -0
  82. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/stdlib/python.json +0 -0
  83. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/stdlib/ruby.json +0 -0
  84. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/stdlib/rust.json +0 -0
  85. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/stdlib/typescript.json +0 -0
  86. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/web/__init__.py +0 -0
  87. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/src/descry/web/web/style.css +0 -0
  88. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/__init__.py +0 -0
  89. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/conftest.py +0 -0
  90. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/eval/README.md +0 -0
  91. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/eval/__init__.py +0 -0
  92. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/eval/compare.py +0 -0
  93. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/eval/corpus.py +0 -0
  94. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/eval/metrics.py +0 -0
  95. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/eval/registry.py +0 -0
  96. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/eval/results/results-descry.json +0 -0
  97. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/eval/results/results-ripgrep.json +0 -0
  98. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/fixtures/small_graph.json +0 -0
  99. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_backtracking.py +0 -0
  100. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_clang_parser.py +0 -0
  101. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_config.py +0 -0
  102. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_cross_lang.py +0 -0
  103. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_dart_parser.py +0 -0
  104. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_dotnet_parser.py +0 -0
  105. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_go_parser.py +0 -0
  106. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_hardening.py +0 -0
  107. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_java_parser.py +0 -0
  108. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_non_regular_files.py +0 -0
  109. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_php_parser.py +0 -0
  110. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_ruby_parser.py +0 -0
  111. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_scip.py +0 -0
  112. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_scip_cache.py +0 -0
  113. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_scip_clang.py +0 -0
  114. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_scip_dart.py +0 -0
  115. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_scip_dotnet.py +0 -0
  116. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_scip_go.py +0 -0
  117. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_scip_java.py +0 -0
  118. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_scip_php.py +0 -0
  119. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_scip_ruby.py +0 -0
  120. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_stdlib.py +0 -0
  121. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tests/test_written_through.py +0 -0
  122. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/README.md +0 -0
  123. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/common.py +0 -0
  124. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/gen_c.py +0 -0
  125. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/gen_cpp.py +0 -0
  126. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/gen_dart.py +0 -0
  127. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/gen_dotnet.py +0 -0
  128. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/gen_go.py +0 -0
  129. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/gen_java.py +0 -0
  130. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/gen_node.py +0 -0
  131. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/gen_php.py +0 -0
  132. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/gen_python.py +0 -0
  133. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/gen_ruby.py +0 -0
  134. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/gen_rust.py +0 -0
  135. {descry_codegraph-0.3.1 → descry_codegraph-0.3.3}/tools/stdlib/gen_typescript.py +0 -0
@@ -6,6 +6,112 @@ 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.3] — 2026-10-23
10
+
11
+ ### Fixed
12
+
13
+ - A `bin` directory is indexed: Dart keeps its entry points there and Rust
14
+ its extra binaries (`src/bin/`). Only the `bin` of a .NET project or of
15
+ an Eclipse project, beside its project file or `.classpath`, is left out
16
+ as build output; every `bin` was.
17
+ - A Dart call of a standard type's name without `new`, `StringBuffer()`,
18
+ makes an instance of that type, as `new StringBuffer()` does. Calls on it
19
+ were taken as calls on an unknown receiver and listed as possible calls
20
+ of every project method of their name (`toString`, `write`, `close`).
21
+
22
+ ## [0.3.2] — 2026-10-23
23
+
24
+ ### Fixed
25
+
26
+ - A caller that reaches a symbol only as one of an unresolved call's
27
+ candidates is counted apart from its callers. `in_degree` ("N callers")
28
+ counts the callers whose calls are resolved to the symbol, and is
29
+ counted again as a graph loads, so a graph from 0.3.1 needs no reindex.
30
+ Every other count gives the others as "(+N possible)": brief `context`
31
+ (callers and callees), `callers`, `callees`, and the web page's headers
32
+ (`possible` in the web API). Lists mark each one; `changes` does so in
33
+ text, in its structured form (`candidates`, `callers_possible`) and on
34
+ the page.
35
+ - A function no longer counts as a certain caller of a symbol it defines
36
+ when its call to it is unresolved.
37
+ - A node ID names that node alone: `callers`, `callees`, `quick`, `flow`
38
+ and `path` given one no longer also match other definitions filed under
39
+ the same owner and name (a Ruby `self.x` beside `x`).
40
+ - `context`'s usage examples are calls of that symbol, those resolved to
41
+ it first, then those it is one of the candidates of, marked; they were
42
+ any unresolved call ending in its name.
43
+ - `callers` of a name lists the calls the resolver left unresolved with no
44
+ candidates apart from its callers, marked "(unresolved; same name)" and
45
+ counted as "+N unresolved" (`unresolved` in the web API). Some are calls
46
+ of the project's definition the resolver cannot follow (a Rust re-export,
47
+ a TypeScript static member), some are calls of something else with that
48
+ name (`loader.list_templates()` on a library's loader). Neither is
49
+ counted as a caller. An unresolved call with candidates reaches only
50
+ those. Callers asked for by node ID are those resolved to it or narrowed
51
+ to it.
52
+ - A list cut short (`callers`, `callees`, `context`'s callers, `changes`,
53
+ backward `flow` in text and as a tree) keeps the callers resolved to the
54
+ symbol first, and says how many it leaves out and how many of those only
55
+ may be; a `flow` tree node gives them as `more` and `more_possible`, and
56
+ the web API's `callers` and `callees` as `more`, shown on the page.
57
+ - `flow`, in text and as a tree, places each symbol at the first level
58
+ that reaches it and marks it "(at level N)" where it is met again
59
+ (`seen`, that level, in the tree); a direct callee or caller reached
60
+ first through another one was dropped from its level, with everything
61
+ below it. `context`, at every depth, lists all of the symbol's own
62
+ callees and cuts each level below to the per-level limit with a note; it
63
+ opens a callee (inlines it and lists its own callees) at the first level
64
+ that reaches it while the token budget covers it, lists every other
65
+ callee by its signature or "(at level N)", and says when the budget, the
66
+ node limit or the timeout cut it short. Depth 1 ignored `max_tokens`. A
67
+ `flow` that stops early says so: "(timeout)" or "(node limit)" in text,
68
+ `cut` in the tree, a note on the page.
69
+ - A `flow` of `depth` N shows N levels of calls below the start; `depth=1`
70
+ showed the start alone. `[query] max_depth` is that many levels too, for
71
+ `flow` and for `context`'s callee expansion. A depth past it is traced to
72
+ it and says so; the web API returns the depth traced, and `/api/health`
73
+ gives the ceiling the page's depth field is held to.
74
+ - A callee the resolver left unresolved is no longer shown or counted as
75
+ certain or as external. `context`, at every depth, names it as one of
76
+ its candidates, or "(unresolved)" where it found none. `callees` marks
77
+ it the same way and counts it apart ("+N unresolved"). A callee is what
78
+ a call is written as: calls written apart (`a.add`, `b.add`) are two
79
+ callees, and calls written alike may reach any definition one of them
80
+ is narrowed to. Forward `flow` follows a possible callee to each
81
+ definition it may reach, marked, and `context` at a depth past 1 says
82
+ how many callees each level leaves out.
83
+ - `callees` says how many callees its `limit` leaves out, and of a name
84
+ several definitions share, which one it answers for (`others` in the web
85
+ API).
86
+ - A `limit`, a `flow` or `context` `depth`, a `context` `max_tokens` or
87
+ `callee_budget`, or a `path` `max_depth` below 1 is refused by every
88
+ tool; `limit=0` answered as if there were no
89
+ callers, matches, changes or history.
90
+ - The web page's changes view no longer throws for a changed symbol with
91
+ no callers.
92
+ - `context --max-output-tokens` cuts the parts the answer is made of, not
93
+ its text: expanded callees, then usage examples past 2, related tests
94
+ and configuration, callers past 5 (still counting possible callers
95
+ apart), the symbol's callees past 5 (planned again for those five, so
96
+ the note counts by kind only callees drawn nowhere in the answer and no
97
+ "(at level N)" mark points at a callee cut away), and the source past 30
98
+ of the symbol's lines, saying what each step left out; a section dropped
99
+ whole keeps its heading with "(omitted, max_output_tokens limit)".
100
+ It read the rendered text back, so code with a heading-like or a fence
101
+ line in it was cut wrongly or not at all, opened callees kept their
102
+ headings without their code, and counts were wrong.
103
+ - `flow` text indents the code it inlines as deep as its fence. The code
104
+ was printed from the first column inside an indented fence, so it broke
105
+ the tree, and below the first level the fence read as an indented code
106
+ block.
107
+ - A command that loads no embedding model no longer imports
108
+ sentence-transformers, and with it torch, as the service starts: where
109
+ they are installed every CLI command spent seconds on it.
110
+ - `churn` ranks symbols, and co-change pairs of symbols, with the same
111
+ number of commits by node ID, so the same history gives the same list.
112
+ Their order among ties changed from run to run, and so, at the limit,
113
+ did which of them were listed.
114
+
9
115
  ## [0.3.1] — 2026-10-22
10
116
 
11
117
  ### Fixed
@@ -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 (2755 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 (2812 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.1
3
+ Version: 0.3.3
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
@@ -267,7 +267,7 @@ For customization, add a `.descry.toml` to your project root.
267
267
 
268
268
  ```toml
269
269
  [project]
270
- # OPTIONAL — REPLACES the 25-element default. Leave unset to keep defaults
270
+ # OPTIONAL — REPLACES the 24-element default. Leave unset to keep defaults
271
271
  # (.git, .gradle, .next, .svelte-kit, .venv, node_modules, target, build, …).
272
272
  # excluded_dirs = ["target", "node_modules", "dist", "build", "vendor"]
273
273
  # OPTIONAL — in a git repository, files git ignores are not indexed.
@@ -393,7 +393,7 @@ Share of project calls resolved without SCIP, measured on public repositories at
393
393
  | C# | serilog `bebc771`, Newtonsoft.Json `52fa3ae` (`Src/Newtonsoft.Json`) | 94%, 91% |
394
394
  | C | jq `a19efdd`, libuv `954c1d1` | 99%, 79% |
395
395
  | C++ | leveldb `7ee830d`, fmt `c4927e6` | 97%, 86% |
396
- | Dart | shelf `b2cb503`, riverpod `1efba1c` (`packages/riverpod`) | 93%, 91% |
396
+ | Dart | shelf `b2cb503`, riverpod `1efba1c` (`packages/riverpod`) | 95%, 91% |
397
397
  | Ruby | rack `a9833c8`, sinatra `cb22afd` | 70%, 39% |
398
398
 
399
399
  The remainder is unresolved, not wrong: of 450 resolved edges drawn at random from these repositories and read against the source, every one reaches the definition the call reaches. Ruby is low because the language declares no types and calls inside DSL blocks run with another `self`; libuv because most of its functions exist once for Unix and once for Windows, and the build files, not the source, choose between them.
@@ -202,7 +202,7 @@ For customization, add a `.descry.toml` to your project root.
202
202
 
203
203
  ```toml
204
204
  [project]
205
- # OPTIONAL — REPLACES the 25-element default. Leave unset to keep defaults
205
+ # OPTIONAL — REPLACES the 24-element default. Leave unset to keep defaults
206
206
  # (.git, .gradle, .next, .svelte-kit, .venv, node_modules, target, build, …).
207
207
  # excluded_dirs = ["target", "node_modules", "dist", "build", "vendor"]
208
208
  # OPTIONAL — in a git repository, files git ignores are not indexed.
@@ -328,7 +328,7 @@ Share of project calls resolved without SCIP, measured on public repositories at
328
328
  | C# | serilog `bebc771`, Newtonsoft.Json `52fa3ae` (`Src/Newtonsoft.Json`) | 94%, 91% |
329
329
  | C | jq `a19efdd`, libuv `954c1d1` | 99%, 79% |
330
330
  | C++ | leveldb `7ee830d`, fmt `c4927e6` | 97%, 86% |
331
- | Dart | shelf `b2cb503`, riverpod `1efba1c` (`packages/riverpod`) | 93%, 91% |
331
+ | Dart | shelf `b2cb503`, riverpod `1efba1c` (`packages/riverpod`) | 95%, 91% |
332
332
  | Ruby | rack `a9833c8`, sinatra `cb22afd` | 70%, 39% |
333
333
 
334
334
  The remainder is unresolved, not wrong: of 450 resolved edges drawn at random from these repositories and read against the source, every one reaches the definition the call reaches. Ruby is low because the language declares no types and calls inside DSL blocks run with another `self`; libuv because most of its functions exist once for Unix and once for Windows, and the build files, not the source, choose between them.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "descry-codegraph"
3
- version = "0.3.1"
3
+ version = "0.3.3"
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.1"
3
+ __version__ = "0.3.3"
4
4
  __all__ = ["__version__"]
@@ -112,4 +112,18 @@ def load_graph_with_schema(path: Path | str) -> dict:
112
112
  f"run 'descry index' to rebuild."
113
113
  )
114
114
  _check_shape(data)
115
+ count_callers(data["nodes"], data["edges"])
115
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",
@@ -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:
@@ -18650,6 +18650,15 @@ class CallResolver:
18650
18650
  if self.standard[lang].key(callee, source_id) == callee:
18651
18651
  held = self._returned_type(constructed, source_id)
18652
18652
  self._result = held or self._result
18653
+ elif verdict[1] == _EXTERNAL and not origin and lang == "dart":
18654
+ # Dart constructs without `new`: a call of a standard type's
18655
+ # name gives an instance of it.
18656
+ library = self.standard[lang]
18657
+ if (
18658
+ callee in library.data["types"]
18659
+ and library.key(callee, source_id) == callee
18660
+ ):
18661
+ self._result = self._returned_type(callee, source_id) or self._result
18653
18662
  elif verdict[1] == _EXTERNAL and not origin and lang == "typescript":
18654
18663
  known, value = self._js_global(callee, source_id)
18655
18664
  if known:
@@ -24878,12 +24887,19 @@ class CodeGraphBuilder:
24878
24887
  for root, dirs, files in os.walk(self.root_dir, followlinks=False):
24879
24888
  dirs.sort()
24880
24889
  files.sort()
24881
- # Filter by name first (excluded dirs, dotfiles), and by what git
24882
- # ignores.
24890
+ # Filter by name first (excluded dirs, dotfiles, the build output
24891
+ # of a .NET or an Eclipse project), and by what git ignores.
24892
+ # `bin` holds sources in Dart and Rust.
24893
+ built = any(
24894
+ f.endswith((".csproj", ".vbproj", ".fsproj")) or f == ".classpath"
24895
+ for f in files
24896
+ )
24883
24897
  dirs[:] = [
24884
24898
  d
24885
24899
  for d in kept(root, dirs)
24886
- if not d.startswith(".") and d not in self.excluded_dirs
24900
+ if not d.startswith(".")
24901
+ and d not in self.excluded_dirs
24902
+ and not (built and d == "bin")
24887
24903
  ]
24888
24904
  # Then filter by realpath containment — drop directories that are
24889
24905
  # symlinks pointing outside the project root.
@@ -25433,19 +25449,10 @@ class CodeGraphBuilder:
25433
25449
  output_path: Path to write the graph JSON
25434
25450
  scip_index: Optional ScipIndex for type-aware resolution
25435
25451
  """
25436
- from descry._graph import CALL_RELATIONS, CURRENT_SCHEMA
25452
+ from descry._graph import CURRENT_SCHEMA, count_callers
25437
25453
 
25438
25454
  self.resolve_references(scip_index=scip_index)
25439
- # The number of its callers: those certain and those it is one of
25440
- # the candidates of
25441
- callers = {}
25442
- for edge in self.edges:
25443
- if edge["relation"] in CALL_RELATIONS:
25444
- candidates = edge.get("metadata", {}).get("candidates", ())
25445
- for target in (edge["target"], *candidates):
25446
- callers.setdefault(target, set()).add(edge["source"])
25447
- for node in self.nodes:
25448
- node["metadata"]["in_degree"] = len(callers.get(node["id"], ()))
25455
+ count_callers(self.nodes, self.edges)
25449
25456
  # Atomic write: stream to a sibling tmp file then os.replace. A
25450
25457
  # concurrent reader (MCP prewarm, web `/api/health`, another CLI
25451
25458
  # 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
- # The callers of this symbol, not of every one of its name
1666
- callers = self.querier.get_callers(node_id)
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,21 +1759,11 @@ class GitHistoryAnalyzer:
1742
1759
  }
1743
1760
 
1744
1761
  if show_callers and self.querier:
1745
- caller_ids = self.querier.get_callers(node_id)
1746
- if caller_ids:
1747
- callers = []
1748
- for caller_id in caller_ids[:5]:
1749
- c_node = self.querier.get_node_info(caller_id)
1750
- c_name = (
1751
- c_node["metadata"].get("name", "?")
1752
- if c_node
1753
- else caller_id.split("::")[-1]
1754
- if "::" in caller_id
1755
- else "?"
1756
- )
1757
- callers.append({"id": caller_id, "name": c_name})
1762
+ callers, total, possible = self._symbol_callers(node_id)
1763
+ if callers:
1758
1764
  entry["callers"] = callers
1759
- entry["callers_total"] = len(caller_ids)
1765
+ entry["callers_total"] = total
1766
+ entry["callers_possible"] = possible
1760
1767
 
1761
1768
  symbols.append(entry)
1762
1769