descry-codegraph 0.3.2__tar.gz → 0.3.4__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.2 → descry_codegraph-0.3.4}/CHANGELOG.md +28 -0
  2. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/CLAUDE.md +1 -1
  3. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/PKG-INFO +3 -3
  4. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/README.md +2 -2
  5. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/claude-skill/descry/references/cli-reference.md +2 -2
  6. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/pyproject.toml +1 -1
  7. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/__init__.py +1 -1
  8. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/cli.py +10 -2
  9. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/embeddings.py +14 -2
  10. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/generate.py +19 -3
  11. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/handlers.py +6 -6
  12. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/query.py +36 -36
  13. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_cli.py +26 -0
  14. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_embeddings.py +20 -0
  15. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_generate.py +48 -0
  16. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_handlers.py +38 -0
  17. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_query.py +14 -2
  18. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/.github/assets/01-landing.png +0 -0
  19. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/.github/workflows/ci.yml +0 -0
  20. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/.github/workflows/release.yml +0 -0
  21. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/.gitignore +0 -0
  22. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/LICENSE +0 -0
  23. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/SECURITY.md +0 -0
  24. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/THIRD-PARTY-NOTICES.md +0 -0
  25. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/claude-skill/README.md +0 -0
  26. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/claude-skill/descry/SKILL.md +0 -0
  27. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/claude-skill/descry/references/mcp-tools.md +0 -0
  28. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/claude-skill/descry/references/recipes.md +0 -0
  29. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/justfile +0 -0
  30. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/pi-extension/README.md +0 -0
  31. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/pi-extension/descry-cli.py +0 -0
  32. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/pi-extension/descry-tools.ts +0 -0
  33. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/_comments.py +0 -0
  34. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/_env.py +0 -0
  35. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/_files.py +0 -0
  36. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/_graph.py +0 -0
  37. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/_stdlib.py +0 -0
  38. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/clang_parser.py +0 -0
  39. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/cross_lang.py +0 -0
  40. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/dart_parser.py +0 -0
  41. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/dotnet_parser.py +0 -0
  42. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/git_history.py +0 -0
  43. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/go_parser.py +0 -0
  44. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/java_parser.py +0 -0
  45. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/mcp_server.py +0 -0
  46. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/php_parser.py +0 -0
  47. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/ruby_parser.py +0 -0
  48. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/__init__.py +0 -0
  49. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapter.py +0 -0
  50. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapters/__init__.py +0 -0
  51. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapters/_empty_environment.json +0 -0
  52. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapters/_java_compat.init.gradle +0 -0
  53. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapters/clang.py +0 -0
  54. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapters/dart.py +0 -0
  55. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapters/dotnet.py +0 -0
  56. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapters/go.py +0 -0
  57. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapters/java.py +0 -0
  58. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapters/php.py +0 -0
  59. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapters/python.py +0 -0
  60. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapters/ruby.py +0 -0
  61. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapters/rust.py +0 -0
  62. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/adapters/typescript.py +0 -0
  63. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/cache.py +0 -0
  64. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/parser.py +0 -0
  65. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/pb2.py +0 -0
  66. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/scip/support.py +0 -0
  67. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/stdlib/c.json +0 -0
  68. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/stdlib/cpp.json +0 -0
  69. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/stdlib/dart.json +0 -0
  70. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/stdlib/dotnet.json +0 -0
  71. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/stdlib/go.json +0 -0
  72. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/stdlib/java.json +0 -0
  73. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/stdlib/node.json +0 -0
  74. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/stdlib/php.json +0 -0
  75. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/stdlib/python.json +0 -0
  76. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/stdlib/ruby.json +0 -0
  77. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/stdlib/rust.json +0 -0
  78. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/stdlib/typescript.json +0 -0
  79. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/web/__init__.py +0 -0
  80. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/web/server.py +0 -0
  81. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/web/web/index.html +0 -0
  82. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/src/descry/web/web/style.css +0 -0
  83. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/__init__.py +0 -0
  84. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/conftest.py +0 -0
  85. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/eval/README.md +0 -0
  86. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/eval/__init__.py +0 -0
  87. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/eval/compare.py +0 -0
  88. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/eval/corpus.py +0 -0
  89. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/eval/metrics.py +0 -0
  90. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/eval/registry.py +0 -0
  91. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/eval/results/results-descry.json +0 -0
  92. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/eval/results/results-ripgrep.json +0 -0
  93. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/fixtures/small_graph.json +0 -0
  94. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_backtracking.py +0 -0
  95. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_clang_parser.py +0 -0
  96. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_config.py +0 -0
  97. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_cross_lang.py +0 -0
  98. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_dart_parser.py +0 -0
  99. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_dotnet_parser.py +0 -0
  100. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_git_history.py +0 -0
  101. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_go_parser.py +0 -0
  102. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_hardening.py +0 -0
  103. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_interfaces.py +0 -0
  104. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_java_parser.py +0 -0
  105. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_mcp.py +0 -0
  106. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_non_regular_files.py +0 -0
  107. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_php_parser.py +0 -0
  108. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_ruby_parser.py +0 -0
  109. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_scip.py +0 -0
  110. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_scip_cache.py +0 -0
  111. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_scip_clang.py +0 -0
  112. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_scip_dart.py +0 -0
  113. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_scip_dotnet.py +0 -0
  114. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_scip_go.py +0 -0
  115. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_scip_java.py +0 -0
  116. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_scip_php.py +0 -0
  117. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_scip_ruby.py +0 -0
  118. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_stdlib.py +0 -0
  119. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_web_server.py +0 -0
  120. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tests/test_written_through.py +0 -0
  121. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/README.md +0 -0
  122. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/common.py +0 -0
  123. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/gen_c.py +0 -0
  124. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/gen_cpp.py +0 -0
  125. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/gen_dart.py +0 -0
  126. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/gen_dotnet.py +0 -0
  127. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/gen_go.py +0 -0
  128. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/gen_java.py +0 -0
  129. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/gen_node.py +0 -0
  130. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/gen_php.py +0 -0
  131. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/gen_python.py +0 -0
  132. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/gen_ruby.py +0 -0
  133. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/gen_rust.py +0 -0
  134. {descry_codegraph-0.3.2 → descry_codegraph-0.3.4}/tools/stdlib/gen_typescript.py +0 -0
@@ -6,6 +6,34 @@ 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.4] — 2026-10-23
10
+
11
+ ### Fixed
12
+
13
+ - `search` answers what semantic search finds when the keywords find
14
+ nothing: the semantic pass ran because they found little, and its
15
+ results were then dropped ("No matches"). It now searches under
16
+ `lang`, `crate`, `type` and `exclude_tests` as the keyword side does; its
17
+ results were merged unfiltered.
18
+ - `search --type` with a type the graph has none of, or one descry does
19
+ not know, matches nothing, as an unknown `lang` does; it was ignored and
20
+ every type answered. The CLI lists the types it takes.
21
+ - `flow --target` takes a node ID as well as a name; a node ID was never
22
+ found, and the trace said nothing of it.
23
+
24
+ ## [0.3.3] — 2026-10-23
25
+
26
+ ### Fixed
27
+
28
+ - A `bin` directory is indexed: Dart keeps its entry points there and Rust
29
+ its extra binaries (`src/bin/`). Only the `bin` of a .NET project or of
30
+ an Eclipse project, beside its project file or `.classpath`, is left out
31
+ as build output; every `bin` was.
32
+ - A Dart call of a standard type's name without `new`, `StringBuffer()`,
33
+ makes an instance of that type, as `new StringBuffer()` does. Calls on it
34
+ were taken as calls on an unknown receiver and listed as possible calls
35
+ of every project method of their name (`toString`, `write`, `close`).
36
+
9
37
  ## [0.3.2] — 2026-10-23
10
38
 
11
39
  ### 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 (2810 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 (2822 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.2
3
+ Version: 0.3.4
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.
@@ -24,7 +24,7 @@ descry search <terms...>
24
24
  [--limit N] # default 10
25
25
  [--lang rust|ts|py|...]
26
26
  [--crate NAME]
27
- [--type Function|Class|Method|...]
27
+ [--type function|class|method|constant|file|all]
28
28
  [--exclude-tests]
29
29
 
30
30
  descry semantic <query>
@@ -49,7 +49,7 @@ descry callees <name> [--limit N] # default 20
49
49
  descry flow <start>
50
50
  [--direction forward|backward] # default forward
51
51
  [--depth N] # default 3, capped by [query] max_depth
52
- [--target NAME] # stop early if reached
52
+ [--target SYMBOL] # stop early if reached (name or node ID)
53
53
  [--inline-threshold N] # inline callee source below N tokens
54
54
 
55
55
  descry path <start> <end>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "descry-codegraph"
3
- version = "0.3.2"
3
+ version = "0.3.4"
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.2"
3
+ __version__ = "0.3.4"
4
4
  __all__ = ["__version__"]
@@ -308,7 +308,9 @@ def main():
308
308
  default=3,
309
309
  help="Levels of calls below the start (default: 3)",
310
310
  )
311
- sub.add_argument("--target", default=None, help="Target symbol to reach")
311
+ sub.add_argument(
312
+ "--target", default=None, help="Symbol to reach, by name or node ID"
313
+ )
312
314
  sub.add_argument(
313
315
  "--inline-threshold",
314
316
  type=int,
@@ -329,7 +331,13 @@ def main():
329
331
  sub.add_argument("--limit", type=int, default=10, help="Max results (default: 10)")
330
332
  sub.add_argument("--lang", default=None, help="Filter by language")
331
333
  sub.add_argument("--crate", default=None, help="Filter by crate")
332
- sub.add_argument("--type", default=None, help="Filter by symbol type")
334
+ sub.add_argument(
335
+ "--type",
336
+ default=None,
337
+ type=str.lower,
338
+ choices=["function", "class", "method", "constant", "file", "all"],
339
+ help="Filter by symbol type",
340
+ )
333
341
  sub.add_argument(
334
342
  "--exclude-tests", action="store_true", help="Exclude test symbols"
335
343
  )
@@ -774,7 +774,9 @@ class SemanticSearcher:
774
774
  self.embeddings = embeddings / np.where(norms == 0, 1, norms)
775
775
  logger.info("Embeddings generated successfully")
776
776
 
777
- def search(self, query: str, limit: int = 10, min_score: float = 0.3) -> list:
777
+ def search(
778
+ self, query: str, limit: int = 10, min_score: float = 0.3, keep=None
779
+ ) -> list:
778
780
  """Search for nodes semantically similar to the query.
779
781
 
780
782
  Args:
@@ -782,6 +784,7 @@ class SemanticSearcher:
782
784
  limit: Maximum number of results
783
785
  min_score: Minimum final score, i.e. cosine similarity after
784
786
  the in-degree and type boosts are added (may exceed 1).
787
+ keep: The node IDs results may have, None for any.
785
788
 
786
789
  Returns:
787
790
  List of (node, score) tuples sorted by relevance
@@ -826,10 +829,19 @@ class SemanticSearcher:
826
829
  np.linalg.norm(self.embeddings, axis=1) * np.linalg.norm(query_embedding)
827
830
  )
828
831
 
832
+ if keep is not None:
833
+ # Nodes outside `keep` never enter the pool
834
+ kept = np.fromiter(
835
+ (n["id"] in keep for n in self.nodes), bool, len(self.nodes)
836
+ )
837
+ similarities = np.where(kept, similarities, -np.inf)
838
+ if not kept.any():
839
+ return []
840
+
829
841
  # Take a candidate pool by base score, then apply min_score to the
830
842
  # final score, so a node whose boosts (worth up to +0.17) lift it over
831
843
  # the threshold is kept.
832
- pool = min(len(similarities), limit * 3)
844
+ pool = min(len(similarities) if keep is None else int(kept.sum()), limit * 3)
833
845
  top_indices = np.argpartition(-similarities, pool - 1)[:pool]
834
846
  top_indices = top_indices[np.argsort(-similarities[top_indices])]
835
847
 
@@ -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.
@@ -92,8 +92,8 @@ _DEFAULT_EXCLUDED_DIRS = {
92
92
  "venv",
93
93
  # Go
94
94
  "vendor",
95
- # .NET
96
- "bin",
95
+ # .NET (its `bin`, and an Eclipse project's, is skipped beside the
96
+ # project file)
97
97
  "obj",
98
98
  # Dart / Flutter
99
99
  ".dart_tool",
@@ -2086,23 +2086,23 @@ class DescryService:
2086
2086
  # and respects the cache lock).
2087
2087
  searcher = await self._get_semantic_searcher()
2088
2088
  if searcher is not None:
2089
+ # Under the filters the keyword side has
2089
2090
  semantic_results = await asyncio.to_thread(
2090
2091
  searcher.search,
2091
2092
  " ".join(terms),
2092
2093
  limit=limit * 2,
2093
2094
  min_score=0.25,
2095
+ keep=q.filtered_ids(lang, crate, symbol_type, exclude_tests),
2094
2096
  )
2095
2097
  except Exception as e: # noqa: BLE001 — boundary over the embedding backend; search degrades to keyword-only
2096
2098
  logger.warning(f"Semantic search failed, using keyword only: {e}")
2097
2099
 
2098
- if semantic_results and tfidf_results:
2100
+ if semantic_results:
2099
2101
  combined = reciprocal_rank_fusion(tfidf_results, semantic_results)
2100
2102
  results = [node for node, _ in combined[:limit]]
2101
2103
  method = "hybrid"
2102
- elif tfidf_results:
2103
- results = tfidf_results[:limit]
2104
2104
  else:
2105
- results = []
2105
+ results = tfidf_results[:limit]
2106
2106
 
2107
2107
  return {"ok": True, "error": None, "results": results, "method": method}
2108
2108
 
@@ -1318,7 +1318,8 @@ class GraphQuerier:
1318
1318
  once, at the first level that reaches it, and met again as `seen`,
1319
1319
  the level of that entry (the start is level 0).
1320
1320
  Returns ``(root, path, cut)``: the tree, the node IDs from the start
1321
- to the first node named ``target``, and why it stopped early
1321
+ to the first node ``target`` names, by name or node ID, and why it
1322
+ stopped early
1322
1323
  ("timeout", "node limit") or None."""
1323
1324
  start_time = time.time()
1324
1325
 
@@ -1353,7 +1354,7 @@ class GraphQuerier:
1353
1354
 
1354
1355
  root = tree_node(start_id, None)
1355
1356
  paths = {start_id: [start_id]}
1356
- if target and root["name"] == target:
1357
+ if target in (root["name"], root["id"]):
1357
1358
  return root, paths[start_id], None
1358
1359
  level = [root]
1359
1360
  for _ in range(depth):
@@ -1378,7 +1379,7 @@ class GraphQuerier:
1378
1379
  child = tree_node(child_id, marks.get(child_id))
1379
1380
  paths[child_id] = [*paths[parent["id"]], child_id]
1380
1381
  parent["children"].append(child)
1381
- if target and child["name"] == target:
1382
+ if target in (child["name"], child["id"]):
1382
1383
  return root, paths[child_id], None
1383
1384
  next_level.append(child)
1384
1385
  level = next_level
@@ -1500,7 +1501,7 @@ class GraphQuerier:
1500
1501
  cut (why the trace stopped early, when it did). Each tree node has: id,
1501
1502
  name, type, tokens, file, line, code, lang, candidates, children,
1502
1503
  more, more_possible, and `seen`, the level its entry is at, where it
1503
- is placed there already. The trace stops at the first node named `target`.
1504
+ is placed there already. The trace stops at the first node `target` names, by name or node ID.
1504
1505
  """
1505
1506
  if timeout_ms is None:
1506
1507
  timeout_ms = self._timeout_ms
@@ -1666,6 +1667,33 @@ class GraphQuerier:
1666
1667
  idf[term] = math.log((total_docs + 1) / (df + 1)) + 1
1667
1668
  return idf
1668
1669
 
1670
+ def filtered_ids(
1671
+ self, lang=None, crate=None, symbol_type=None, exclude_tests=False
1672
+ ):
1673
+ """The IDs of the nodes `search_docs`'s filters keep, None when none
1674
+ is given: the language, the crate or module, the type, and no tests."""
1675
+ if not (lang or crate or symbol_type or exclude_tests):
1676
+ return None
1677
+ self._ensure_filter_indices()
1678
+ kept = set(self.nodes.keys())
1679
+ if exclude_tests and self._index_is_test:
1680
+ kept -= self._index_is_test
1681
+ if crate:
1682
+ kept &= self._index_by_crate.get(crate, set())
1683
+ if lang and lang != "all":
1684
+ lang = _LANG_ALIASES.get(lang.lower(), lang.lower())
1685
+ kept &= self._index_by_lang.get(lang, set())
1686
+ if symbol_type and symbol_type != "all":
1687
+ type_map = {
1688
+ "function": "Function",
1689
+ "class": "Class",
1690
+ "method": "Method",
1691
+ "constant": "Constant",
1692
+ "file": "File",
1693
+ }
1694
+ kept &= self._index_by_type.get(type_map.get(symbol_type.lower()), set())
1695
+ return kept
1696
+
1669
1697
  def search_docs(
1670
1698
  self, query_terms, lang=None, crate=None, symbol_type=None, exclude_tests=False
1671
1699
  ):
@@ -1695,38 +1723,10 @@ class GraphQuerier:
1695
1723
  if num_terms == 0:
1696
1724
  return []
1697
1725
 
1698
- # Apply filters if any specified
1699
- if lang or crate or symbol_type or exclude_tests:
1700
- self._ensure_filter_indices()
1701
- candidate_ids = set(self.nodes.keys())
1702
-
1703
- # Filter out test files and functions if requested
1704
- if exclude_tests and self._index_is_test:
1705
- candidate_ids -= self._index_is_test
1706
-
1707
- if crate:
1708
- # If crate filter specified but doesn't exist, return empty results
1709
- if crate not in self._index_by_crate:
1710
- return []
1711
- candidate_ids &= self._index_by_crate[crate]
1712
- if lang and lang != "all":
1713
- lang = _LANG_ALIASES.get(lang.lower(), lang.lower())
1714
- candidate_ids &= self._index_by_lang.get(lang, set())
1715
- if symbol_type and symbol_type != "all":
1716
- type_map = {
1717
- "function": "Function",
1718
- "class": "Class",
1719
- "method": "Method",
1720
- "constant": "Constant",
1721
- "file": "File",
1722
- }
1723
- type_key = type_map.get(symbol_type.lower())
1724
- if type_key and type_key in self._index_by_type:
1725
- candidate_ids &= self._index_by_type[type_key]
1726
-
1727
- candidates = [self.nodes[nid] for nid in candidate_ids]
1728
- else:
1729
- candidates = self.data["nodes"]
1726
+ kept = self.filtered_ids(lang, crate, symbol_type, exclude_tests)
1727
+ candidates = (
1728
+ self.data["nodes"] if kept is None else [self.nodes[nid] for nid in kept]
1729
+ )
1730
1730
 
1731
1731
  for node in candidates:
1732
1732
  score = 0.0
@@ -225,3 +225,29 @@ class TestCliStopped:
225
225
  index.send_signal(signal.SIGHUP)
226
226
  assert not indexer_tree.grandchild_exited(timeout=1)
227
227
  assert index.poll() is None
228
+
229
+
230
+ class TestSearchType:
231
+ """`--type` takes the types descry has, in any case, and refuses others."""
232
+
233
+ def test_a_type_is_passed_lowercased(self, monkeypatch):
234
+ seen = {}
235
+
236
+ class Service:
237
+ async def search(self, **kwargs):
238
+ seen.update(kwargs)
239
+ return "ok"
240
+
241
+ monkeypatch.setattr(cli, "_make_service", Service)
242
+ monkeypatch.setattr(
243
+ sys, "argv", ["descry", "search", "x", "--type", "Function"]
244
+ )
245
+ cli.main()
246
+ assert seen["symbol_type"] == "function"
247
+
248
+ def test_an_unknown_type_is_refused(self, monkeypatch, capsys):
249
+ monkeypatch.setattr(sys, "argv", ["descry", "search", "x", "--type", "struct"])
250
+ with pytest.raises(SystemExit) as exit_:
251
+ cli.main()
252
+ assert exit_.value.code == 2
253
+ assert "invalid choice: 'struct'" in capsys.readouterr().err
@@ -434,6 +434,26 @@ class TestSearchScoring:
434
434
  with pytest.raises(EmbeddingCacheMismatch):
435
435
  s.search("anything", limit=3)
436
436
 
437
+ def test_kept_nodes_are_the_pool_others_never_enter(self, fake_model, graph_writer):
438
+ """A filter applies before the pool is cut, so a kept node far down
439
+ the ranking is still found."""
440
+ s = SemanticSearcher(str(graph_writer()))
441
+ dim = s.embeddings.shape[1]
442
+ s.embeddings = np.zeros((len(s.nodes), dim), dtype="float32")
443
+ s.embeddings[:, 0] = 1.0
444
+ last = len(s.nodes) - 1
445
+ s.embeddings[last] = 0.0
446
+ s.embeddings[last, 1] = 1.0
447
+ q = np.zeros(dim, dtype="float32")
448
+ q[0], q[1] = 1.0, 0.1
449
+ s.model = FakeSentenceTransformer(dim=dim, prompts={})
450
+ s.model.encode = lambda texts, **kw: q.reshape(1, -1)
451
+ wanted = s.nodes[last]["id"]
452
+
453
+ hits = s.search("q", limit=1, min_score=-1.0, keep={wanted})
454
+ assert [n["id"] for n, _ in hits] == [wanted]
455
+ assert s.search("q", limit=1, min_score=-1.0, keep=set()) == []
456
+
437
457
  def test_results_are_sorted_and_limited(self, fake_model, graph_writer):
438
458
  s = SemanticSearcher(str(graph_writer()))
439
459
  hits = s.search("alpha", limit=2, min_score=-1.0)
@@ -30858,3 +30858,51 @@ def test_a_form_feed_ends_no_line(tmp_path):
30858
30858
  }
30859
30859
  assert len(found) == 11
30860
30860
  assert set(found.values()) == {3}
30861
+
30862
+
30863
+ def test_a_bin_directory_is_indexed_unless_it_is_dotnet_build_output(tmp_path):
30864
+ """Dart's `bin/` and Rust's `src/bin/` hold sources; a .NET or an
30865
+ Eclipse project's `bin/` holds what it builds."""
30866
+ from descry.handlers import _DEFAULT_EXCLUDED_DIRS
30867
+
30868
+ files = {
30869
+ "dart/bin/main.dart": "void main() {}\n",
30870
+ "rust/src/bin/tool.rs": "fn main() {}\n",
30871
+ "net/App.csproj": "<Project />\n",
30872
+ "net/bin/Debug/Copied.cs": "class Copied {}\n",
30873
+ "eclipse/.classpath": "<classpath />\n",
30874
+ "eclipse/bin/generated/Copied.java": "class Copied {}\n",
30875
+ }
30876
+ for rel, source in files.items():
30877
+ (tmp_path / rel).parent.mkdir(parents=True, exist_ok=True)
30878
+ (tmp_path / rel).write_text(source)
30879
+ builder = CodeGraphBuilder(tmp_path, excluded_dirs=set(_DEFAULT_EXCLUDED_DIRS))
30880
+ builder.process_directory()
30881
+ found = {n["id"] for n in builder.nodes if n["type"] == "File"}
30882
+ assert {"FILE:dart/bin/main.dart", "FILE:rust/src/bin/tool.rs"} <= found
30883
+ assert not any(
30884
+ "/bin/" in f for f in found if f.startswith(("FILE:net", "FILE:eclipse"))
30885
+ )
30886
+
30887
+
30888
+ def test_a_dart_call_of_a_standard_type_constructs_it(tmp_path):
30889
+ """Dart needs no `new`: `StringBuffer()` makes a StringBuffer, whose
30890
+ `toString` is the library's, not a project method of that name."""
30891
+ builder = _index(
30892
+ tmp_path,
30893
+ {
30894
+ "pubspec.yaml": "name: x\n",
30895
+ "lib/a.dart": """
30896
+ class Oops {
30897
+ @override
30898
+ String toString() => 'oops';
30899
+ }
30900
+
30901
+ String build() {
30902
+ final sb = StringBuffer();
30903
+ return sb.toString();
30904
+ }
30905
+ """,
30906
+ },
30907
+ )
30908
+ assert _calls(builder, "FILE:lib/a.dart::build") == []
@@ -1709,3 +1709,41 @@ async def test_a_context_budget_below_one_is_refused(tmp_path, bound):
1709
1709
  result = await DescryService(config).context("FILE:x.py::f", depth=2, **{bound: 0})
1710
1710
  assert isinstance(result, ServiceError)
1711
1711
  assert f"{bound} must be at least 1, got 0." in result
1712
+
1713
+
1714
+ @pytest.mark.asyncio
1715
+ @pytest.mark.parametrize(
1716
+ ("terms", "lang", "found"),
1717
+ [
1718
+ (["who", "talks", "to", "nothing"], None, ["FILE:x.py::f", "FILE:y.rs::g"]),
1719
+ (["who", "talks", "to", "nothing"], "python", ["FILE:x.py::f"]),
1720
+ # The keywords find `f` too; the semantic side is still filtered
1721
+ (["f"], "python", ["FILE:x.py::f"]),
1722
+ ],
1723
+ )
1724
+ async def test_search_keeps_semantic_matches_no_keyword_finds(
1725
+ tmp_path, terms, lang, found
1726
+ ):
1727
+ """The semantic side runs because the keywords found little; what it
1728
+ finds is answered, under the same filters as the keywords."""
1729
+ config = _plain_config(tmp_path)
1730
+ py = {"id": "FILE:x.py::f", "type": "Function", "metadata": {"name": "f"}}
1731
+ rs = {"id": "FILE:y.rs::g", "type": "Function", "metadata": {"name": "g"}}
1732
+ _write_graph(config, nodes=[py, rs])
1733
+ svc = DescryService(config)
1734
+
1735
+ class Searcher:
1736
+ def __init__(self, graph_path, **kwargs):
1737
+ pass
1738
+
1739
+ def ensure_model(self):
1740
+ pass
1741
+
1742
+ def search(self, query, limit=10, min_score=0.0, keep=None):
1743
+ return [
1744
+ r for r in [(py, 0.9), (rs, 0.8)] if keep is None or r[0]["id"] in keep
1745
+ ]
1746
+
1747
+ svc._semantic_available, svc._SemanticSearcher = True, Searcher
1748
+ data = await svc.search_data(terms, lang=lang)
1749
+ assert ([n["id"] for n in data["results"]], data["method"]) == (found, "hybrid")
@@ -301,12 +301,16 @@ class TestFindCallPath:
301
301
 
302
302
  os.unlink(f.name)
303
303
 
304
+ @pytest.mark.parametrize(
305
+ "target", ["validate_token", "FILE:auth.rs::validate_token"]
306
+ )
304
307
  def test_structured_flow_stops_at_the_target_and_reports_the_path(
305
- self, call_path_graph
308
+ self, call_path_graph, target
306
309
  ):
310
+ """The target is named by name or by node ID."""
307
311
  q = GraphQuerier(call_path_graph)
308
312
 
309
- tree = q.trace_flow_structured("handle_request", target="validate_token")
313
+ tree = q.trace_flow_structured("handle_request", target=target)
310
314
 
311
315
  assert tree["path_to_target"] == [
312
316
  "handle_request",
@@ -1073,6 +1077,14 @@ class TestLanguageFilter:
1073
1077
  def test_all_and_none_do_not_filter(self, querier, lang):
1074
1078
  assert len(querier.search_docs(["handler"], lang=lang)) == len(self.FILES)
1075
1079
 
1080
+ @pytest.mark.parametrize(
1081
+ ("kind", "found"), [("function", len(FILES)), ("class", 0), ("struct", 0)]
1082
+ )
1083
+ def test_type_keeps_only_that_type(self, querier, kind, found):
1084
+ """A type the graph has none of, or no type descry has, matches
1085
+ nothing, as an unknown language does."""
1086
+ assert len(querier.search_docs(["handler"], symbol_type=kind)) == found
1087
+
1076
1088
 
1077
1089
  class TestCallPathDepthAndOutcome:
1078
1090
  def _chain(self, tmp_path, length, **config):