descry-codegraph 1.1.0__tar.gz → 1.2.1__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 (222) hide show
  1. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/.github/workflows/release.yml +3 -3
  2. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/AGENTS.md +3 -3
  3. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/CHANGELOG.md +53 -0
  4. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/PKG-INFO +10 -8
  5. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/README.md +9 -7
  6. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/claude-skill/README.md +1 -1
  7. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/claude-skill/descry/SKILL.md +7 -7
  8. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/claude-skill/descry/references/cli-reference.md +2 -1
  9. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/claude-skill/descry/references/mcp-tools.md +1 -1
  10. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/claude-skill/descry/references/recipes.md +3 -3
  11. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/docs/CERTAINTY.md +5 -4
  12. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/docs/TOOLS.md +1 -1
  13. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/pi-extension/descry-tools.ts +1 -1
  14. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/pyproject.toml +4 -1
  15. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/__init__.py +1 -1
  16. descry_codegraph-1.2.1/src/descry/_files.py +158 -0
  17. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/cli.py +2 -0
  18. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/embeddings.py +2 -79
  19. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/generate.py +60 -33
  20. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/handlers.py +43 -15
  21. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/mcp_server.py +1 -1
  22. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/resolver.py +12 -2
  23. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_cli.py +14 -1
  24. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_embeddings.py +82 -10
  25. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_generate.py +56 -1
  26. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_handlers.py +49 -2
  27. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_parse_cache.py +29 -0
  28. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_web_server.py +4 -5
  29. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_written_through.py +39 -5
  30. descry_codegraph-1.1.0/src/descry/_files.py +0 -68
  31. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/.github/assets/01-landing.png +0 -0
  32. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/.github/workflows/ci.yml +0 -0
  33. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/.gitignore +0 -0
  34. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/CONTRACT.md +0 -0
  35. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/LICENSE +0 -0
  36. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/SECURITY.md +0 -0
  37. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/THIRD-PARTY-NOTICES.md +0 -0
  38. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/docs/UPGRADING.md +0 -0
  39. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/justfile +0 -0
  40. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/pi-extension/README.md +0 -0
  41. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/pi-extension/descry-cli.py +0 -0
  42. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/_answers.py +0 -0
  43. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/_comments.py +0 -0
  44. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/_env.py +0 -0
  45. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/_graph.py +0 -0
  46. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/_parse_cache.py +0 -0
  47. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/_parsing.py +0 -0
  48. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/_reads.py +0 -0
  49. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/_stdlib.py +0 -0
  50. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/_streams.py +0 -0
  51. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/clang_parser.py +0 -0
  52. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/cross_lang.py +0 -0
  53. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/dart_parser.py +0 -0
  54. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/dotnet_parser.py +0 -0
  55. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/git_history.py +0 -0
  56. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/go_parser.py +0 -0
  57. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/java_parser.py +0 -0
  58. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/php_parser.py +0 -0
  59. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/proto_parser.py +0 -0
  60. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/python_parser.py +0 -0
  61. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/query.py +0 -0
  62. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/ruby_parser.py +0 -0
  63. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/rust_parser.py +0 -0
  64. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/__init__.py +0 -0
  65. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapter.py +0 -0
  66. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapters/__init__.py +0 -0
  67. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapters/_empty_environment.json +0 -0
  68. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapters/_java_compat.init.gradle +0 -0
  69. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapters/clang.py +0 -0
  70. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapters/dart.py +0 -0
  71. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapters/dotnet.py +0 -0
  72. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapters/go.py +0 -0
  73. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapters/java.py +0 -0
  74. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapters/php.py +0 -0
  75. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapters/python.py +0 -0
  76. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapters/ruby.py +0 -0
  77. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapters/rust.py +0 -0
  78. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/adapters/typescript.py +0 -0
  79. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/cache.py +0 -0
  80. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/parser.py +0 -0
  81. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/pb2.py +0 -0
  82. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/scip/support.py +0 -0
  83. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/stdlib/c.json +0 -0
  84. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/stdlib/cpp.json +0 -0
  85. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/stdlib/dart.json +0 -0
  86. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/stdlib/dotnet.json +0 -0
  87. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/stdlib/go.json +0 -0
  88. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/stdlib/java.json +0 -0
  89. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/stdlib/node.json +0 -0
  90. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/stdlib/php.json +0 -0
  91. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/stdlib/python.json +0 -0
  92. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/stdlib/ruby.json +0 -0
  93. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/stdlib/rust.json +0 -0
  94. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/stdlib/typescript.json +0 -0
  95. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/ts_parser.py +0 -0
  96. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/web/__init__.py +0 -0
  97. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/web/server.py +0 -0
  98. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/web/web/index.html +0 -0
  99. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/src/descry/web/web/style.css +0 -0
  100. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/__init__.py +0 -0
  101. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/conftest.py +0 -0
  102. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/eval/README.md +0 -0
  103. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/eval/__init__.py +0 -0
  104. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/eval/compare.py +0 -0
  105. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/eval/corpus.py +0 -0
  106. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/eval/metrics.py +0 -0
  107. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/eval/registry.py +0 -0
  108. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/eval/results/results-descry.json +0 -0
  109. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/eval/results/results-ripgrep.json +0 -0
  110. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/eval/results/search-after.json +0 -0
  111. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/eval/results/search-before.json +0 -0
  112. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/eval/search_eval.py +0 -0
  113. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/eval/search_questions.json +0 -0
  114. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/fixtures/small_graph.json +0 -0
  115. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/c/Makefile +0 -0
  116. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/c/expected.json +0 -0
  117. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/c/io.c +0 -0
  118. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/c/io.h +0 -0
  119. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/c/main.c +0 -0
  120. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/c/order.c +0 -0
  121. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/cpp/Makefile +0 -0
  122. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/cpp/expected.json +0 -0
  123. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/cpp/main.cpp +0 -0
  124. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/cpp/models.hpp +0 -0
  125. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/cpp/reader.hpp +0 -0
  126. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/csharp/Gt.csproj +0 -0
  127. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/csharp/Io.cs +0 -0
  128. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/csharp/Models.cs +0 -0
  129. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/csharp/Program.cs +0 -0
  130. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/csharp/expected.json +0 -0
  131. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/dart/bin/main.dart +0 -0
  132. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/dart/expected.json +0 -0
  133. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/dart/lib/io.dart +0 -0
  134. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/dart/lib/models.dart +0 -0
  135. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/dart/pubspec.yaml +0 -0
  136. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/go/expected.json +0 -0
  137. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/go/go.mod +0 -0
  138. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/go/io.go +0 -0
  139. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/go/main.go +0 -0
  140. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/go/models.go +0 -0
  141. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/java/expected.json +0 -0
  142. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/java/src/gt/Base.java +0 -0
  143. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/java/src/gt/Io.java +0 -0
  144. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/java/src/gt/Main.java +0 -0
  145. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/java/src/gt/Order.java +0 -0
  146. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/java/src/gt/Reader.java +0 -0
  147. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/java/src/gt/User.java +0 -0
  148. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/javascript/expected.json +0 -0
  149. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/javascript/package.json +0 -0
  150. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/javascript/src/io.js +0 -0
  151. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/javascript/src/main.js +0 -0
  152. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/javascript/src/models.js +0 -0
  153. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/php/composer.json +0 -0
  154. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/php/expected.json +0 -0
  155. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/php/src/Io.php +0 -0
  156. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/php/src/Models.php +0 -0
  157. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/php/src/main.php +0 -0
  158. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/python/app/__init__.py +0 -0
  159. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/python/app/io.py +0 -0
  160. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/python/app/main.py +0 -0
  161. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/python/app/models.py +0 -0
  162. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/python/expected.json +0 -0
  163. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/python/pyproject.toml +0 -0
  164. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/ruby/Gemfile +0 -0
  165. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/ruby/expected.json +0 -0
  166. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/ruby/lib/io.rb +0 -0
  167. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/ruby/lib/main.rb +0 -0
  168. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/ruby/lib/models.rb +0 -0
  169. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/rust/Cargo.toml +0 -0
  170. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/rust/expected.json +0 -0
  171. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/rust/src/io.rs +0 -0
  172. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/rust/src/main.rs +0 -0
  173. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/rust/src/models.rs +0 -0
  174. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/typescript/expected.json +0 -0
  175. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/typescript/package.json +0 -0
  176. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/typescript/src/io.ts +0 -0
  177. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/typescript/src/main.ts +0 -0
  178. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/ground_truth/typescript/src/models.ts +0 -0
  179. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_backtracking.py +0 -0
  180. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_clang_parser.py +0 -0
  181. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_config.py +0 -0
  182. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_cross_lang.py +0 -0
  183. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_dart_parser.py +0 -0
  184. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_docs.py +0 -0
  185. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_dotnet_parser.py +0 -0
  186. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_git_history.py +0 -0
  187. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_go_parser.py +0 -0
  188. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_ground_truth.py +0 -0
  189. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_hardening.py +0 -0
  190. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_interfaces.py +0 -0
  191. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_java_parser.py +0 -0
  192. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_mcp.py +0 -0
  193. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_non_regular_files.py +0 -0
  194. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_php_parser.py +0 -0
  195. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_query.py +0 -0
  196. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_resolve_cache.py +0 -0
  197. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_ruby_parser.py +0 -0
  198. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_scip.py +0 -0
  199. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_scip_cache.py +0 -0
  200. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_scip_clang.py +0 -0
  201. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_scip_dart.py +0 -0
  202. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_scip_dotnet.py +0 -0
  203. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_scip_go.py +0 -0
  204. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_scip_java.py +0 -0
  205. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_scip_php.py +0 -0
  206. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_scip_ruby.py +0 -0
  207. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tests/test_stdlib.py +0 -0
  208. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/docs/gen_tools.py +0 -0
  209. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/README.md +0 -0
  210. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/common.py +0 -0
  211. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/gen_c.py +0 -0
  212. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/gen_cpp.py +0 -0
  213. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/gen_dart.py +0 -0
  214. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/gen_dotnet.py +0 -0
  215. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/gen_go.py +0 -0
  216. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/gen_java.py +0 -0
  217. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/gen_node.py +0 -0
  218. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/gen_php.py +0 -0
  219. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/gen_python.py +0 -0
  220. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/gen_ruby.py +0 -0
  221. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/gen_rust.py +0 -0
  222. {descry_codegraph-1.1.0 → descry_codegraph-1.2.1}/tools/stdlib/gen_typescript.py +0 -0
@@ -32,16 +32,16 @@ jobs:
32
32
  - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
33
33
  with:
34
34
  python-version: "3.12"
35
- - name: Verify tag matches pyproject version
35
+ - name: Verify tag matches package version
36
36
  # A manual run that targets PyPI must be dispatched from the release tag.
37
37
  if: github.event_name == 'push' || inputs.target == 'pypi'
38
38
  env:
39
39
  GIT_TAG: ${{ github.ref_name }}
40
40
  run: |
41
- PKG_VERSION=$(python -c "import tomllib; print(tomllib.loads(open('pyproject.toml','rb').read().decode())['project']['version'])")
41
+ PKG_VERSION=$(PYTHONPATH=src python -c "import descry; print(descry.__version__)")
42
42
  TAG_VERSION="${GIT_TAG#v}"
43
43
  if [ "$PKG_VERSION" != "$TAG_VERSION" ]; then
44
- echo "::error::tag $GIT_TAG does not match pyproject version $PKG_VERSION"
44
+ echo "::error::tag $GIT_TAG does not match package version $PKG_VERSION"
45
45
  exit 1
46
46
  fi
47
47
  - name: Build sdist + wheel
@@ -23,7 +23,7 @@ just test tests/test_config.py::TestFromToml::test_from_toml_basic # 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 (3343 tests pass; a handful more are skipped where the environment lacks their fixture — 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 (3382 tests pass; a handful more are skipped where the environment lacks their fixture — a drop is a regression signal).
27
27
 
28
28
  Invoke the tool while developing:
29
29
 
@@ -43,7 +43,7 @@ Packaging: `pyproject.toml` defines four console scripts — `descry`, `descry-m
43
43
 
44
44
  **Two graph phases:**
45
45
 
46
- 1. **Generate** (`generate.py`, invoked via `descry index` or `descry-generate`) — walks the project, parses source files with regex + AST (`_parsing.py` holds what every parser shares; each language has its `<lang>_parser.py`), resolves calls (`CallResolver` in `resolver.py`) from imports, declared and returned types, scope and the class hierarchy, optionally runs SCIP indexers (rust-analyzer, scip-typescript) for type-aware call resolution, optionally builds embeddings. Writes `.descry_cache/codebase_graph.json`. A call is resolved, left unresolved with the definitions that fit recorded as `candidates`, or dropped as external. Never resolve by name alone when more than one definition fits, never by file path, and never guess the type of a receiver: an unknown receiver stays unresolved.
46
+ 1. **Generate** (`generate.py`, invoked via `descry index` or `descry-generate`) — walks the project, parses source files with regex + AST (`_parsing.py` holds what every parser shares; each language has its `<lang>_parser.py`), resolves calls (`CallResolver` in `resolver.py`) from imports, declared and returned types, scope and the class hierarchy, optionally runs SCIP indexers (rust-analyzer, scip-typescript) for type-aware call resolution, optionally builds embeddings. Writes `.descry_cache/codebase_graph.json` last, once its embeddings are written (the graph is built under a temporary name and dated when the walk began), and holds `index.lock` for the whole run. A call is resolved, left unresolved with the definitions that fit recorded as `candidates`, or dropped as external. Never resolve by name alone when more than one definition fits, never by file path, and never guess the type of a receiver: an unknown receiver stays unresolved.
47
47
  2. **Query** (`query.py` → `GraphQuerier`) — loads the cached graph and answers callers/callees/flow/path/context queries. All service methods lazy-load the querier; freshness is tracked by graph file mtime.
48
48
 
49
49
  **Graph schema is versioned.** `src/descry/_graph.py` exports `CURRENT_SCHEMA` (currently `2`) and `load_graph_with_schema()`. Every graph-consuming code path must load via this helper — it raises `GraphSchemaError` on mismatch so stale graphs are rejected rather than silently producing wrong results. If you change the graph shape in a breaking way, bump `CURRENT_SCHEMA`.
@@ -70,7 +70,7 @@ These are load-bearing — see the `### Security` sections of `CHANGELOG.md`. Do
70
70
  - **The default cache directory stays inside the project.** A repository can ship `.descry_cache`, or its `scip` or `parse` subdirectory, as a symlink to another directory; `DescryConfig.cache_dir_error()` refuses indexing and embedding there, and only `DESCRY_CACHE_DIR` names a cache outside the project.
71
71
  - **A parse is reused only from an entry descry wrote.** Each file's parse is kept in `<cache>/parse` as JSON, never pickle, with an HMAC of the entry's name and content under a key in the user's state directory (`$XDG_STATE_HOME/descry/parse.key` when that is absolute, else `~/.local/state`; `%LOCALAPPDATA%` on Windows); where the key would be inside the project, no entry is read or kept. An entry a repository ships, or one moved to another file's name, fails the check and the file is parsed again; restoring an entry cannot stop an index.
72
72
  - **TOML-sourced subprocess args are validated.** `_validate_toolchain`, `_validate_scip_extra_arg`, `_validate_embedding_model` in `handlers.py` reject short flags, shell metacharacters, and out-of-root local paths. New TOML → subprocess plumbing must validate.
73
- - **Web UI is local-only by design.** `descry-web` binds `127.0.0.1`, has no auth, and deliberately omits `CORSMiddleware` so browsers enforce same-origin — a tab on `evil.com` cannot read `/api/source`. State-changing requests are refused unless same-origin (`Origin` / `Sec-Fetch-Site`), so it cannot trigger `/api/index` either. The host middleware rejects non-loopback `Host` headers to defeat DNS-rebinding that would otherwise bypass the 127.0.0.1 bind. `/api/source` enforces project-root containment, rejects non-regular files, files over `[code_files] max_file_mb` (10 by default; 0, which a repository's `.descry.toml` can set, lifts the cap) and files with a NUL byte in their first 8 KiB, and opens every path component with `O_NOFOLLOW` (on Windows, which has no `openat`, it checks after opening that the file opened is the one the path resolves to, inside the root). `/api/index` (and `/api/index/stream`) take no path parameter (always indexes the configured root). `DescryService` runs one index at a time for every interface and stops the indexer's whole process tree when a run ends early. The page's remote scripts carry SRI hashes and every response carries the Content-Security-Policy that `web/server.py` builds from the page's own tags, allowing exactly those asset URLs; a new remote script needs an SRI hash. Don't add a `--host 0.0.0.0` flag, don't add `CORSMiddleware`, and don't expose network listeners.
73
+ - **Web UI is local-only by design.** `descry-web` binds `127.0.0.1`, has no auth, and deliberately omits `CORSMiddleware` so browsers enforce same-origin — a tab on `evil.com` cannot read `/api/source`. State-changing requests are refused unless same-origin (`Origin` / `Sec-Fetch-Site`), so it cannot trigger `/api/index` either. The host middleware rejects non-loopback `Host` headers to defeat DNS-rebinding that would otherwise bypass the 127.0.0.1 bind. `/api/source` enforces project-root containment, rejects non-regular files, files over `[code_files] max_file_mb` (10 by default; 0, which a repository's `.descry.toml` can set, lifts the cap) and files with a NUL byte in their first 8 KiB, and opens every path component with `O_NOFOLLOW` (on Windows, which has no `openat`, it checks after opening that the file opened is the one the path resolves to, inside the root). `/api/index` (and `/api/index/stream`) take no path parameter (always indexes the configured root). `DescryService` runs one index at a time for every interface, refuses one while another process holds the project's `index.lock`, and stops the indexer's whole process tree when a run ends early. The page's remote scripts carry SRI hashes and every response carries the Content-Security-Policy that `web/server.py` builds from the page's own tags, allowing exactly those asset URLs; a new remote script needs an SRI hash. Don't add a `--host 0.0.0.0` flag, don't add `CORSMiddleware`, and don't expose network listeners.
74
74
  - **MCP `index(path=...)`** is restricted to the configured project root; a path below it narrows the walk but still writes the root's graph.
75
75
  - **Embedding cache** uses numpy safe-mode load + JSON sidecar + atomic writes, keyed by the graph's bytes and time and the recipe. A rebuild reuses a node's vector, by the digest of its text, only from a cache descry signed with the key in the user's state directory (`signing_key()` in `_parse_cache.py`), so a shipped cache is never reused. `MODEL_REGISTRY` in `embeddings.py` is the single source of truth for which models descry ships pinned configurations for — each entry carries its revision and its task prompts. **`trust_remote_code` is always `False`**: there is no registry field, loader parameter or config key that enables it, so descry never executes code from a model repository — do not add one. A model named in `.descry.toml` that is not registered is loaded unpinned. Adding a model means adding a registry entry, not editing the loader.
76
76
  - **The cache key is the embedding recipe's identity.** `_recipe_fingerprint()` folds in the resolved repo id, the pinned revision, `RECIPE_VERSION`, `DOCSTRING_CHAR_LIMIT`, the prompt names, the entry's loading options, and whether source code is embedded (`CODE_CHAR_LIMIT`). Anything that changes the vectors without changing the graph must be added there and `RECIPE_VERSION` bumped, or a stale cache is served against freshly-encoded queries — comparing two different embedding spaces.
@@ -4,6 +4,59 @@ All notable changes to Descry will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [semver](https://semver.org/) from 1.0.0.
6
6
 
7
+ ## [1.2.1] — 2026-11-13
8
+
9
+ ### Fixed
10
+
11
+ - Python: an index no longer ends with `RecursionError` on a class statement
12
+ in a method whose base is written through `self` or `cls`
13
+ (`class Extra(self.form_class)`). The base is read in the scope around the
14
+ class statement, as Python evaluates it.
15
+ - Python: an index no longer ends with `TypeError: unhashable type: 'list'`
16
+ where a method assigns an attribute that a base declares as a property
17
+ with a setter and then reads it.
18
+
19
+ ## [1.2.0] — 2026-11-04
20
+
21
+ ### Added
22
+
23
+ - `descry --version`; the skill's CLI reference lists it.
24
+
25
+ ### Changed
26
+
27
+ - An index publishes the graph only once its embeddings are written, so
28
+ searches during a re-index stay hybrid on the previous graph. Before, the
29
+ new graph appeared first: searches fell back to keyword-only until the
30
+ embeddings were done, one stalled for `[timeouts] embedding_seconds`
31
+ (60 s by default), and after ten minutes `semantic` reported the model
32
+ unavailable.
33
+ - During a first index, a tool that needs the graph and `status` answer
34
+ "No graph yet: an index is building it", and the text header reads
35
+ `[Graph: BUILDING]`, instead of saying no graph exists.
36
+ - An index of a project another process is indexing is refused as `busy`
37
+ (`index`, `ensure`, the web's index stream, and `descry-generate`).
38
+ - `ensure` re-indexes when files the graph tracks changed or were removed
39
+ since it was written, as well as when it is older than `max_stale_hours`.
40
+ A file dated in the future is not counted as changed, there or in
41
+ `status`.
42
+ - The graph is dated when its index began, so a file edited while an index
43
+ runs counts as changed since it; before, an edit made before the graph
44
+ was written was missed.
45
+
46
+ ### Security
47
+
48
+ - Windows: a lock file in the cache directory (`embeddings.lock`,
49
+ `index.lock`) that the repository ships as a symlink is refused instead
50
+ of followed, so an index neither locks nor creates the file it names, as
51
+ on other platforms.
52
+
53
+ ### Documentation
54
+
55
+ - README: the MCP server is `descry-mcp.exe` on Windows, and
56
+ `python -m descry.mcp_server` starts it on every platform. Install
57
+ lines quote the extra (`"descry-codegraph[all]"`) so zsh does not
58
+ read the brackets as a glob.
59
+
7
60
  ## [1.1.0] — 2026-11-03
8
61
 
9
62
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: descry-codegraph
3
- Version: 1.1.0
3
+ Version: 1.2.1
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
@@ -165,7 +165,7 @@ yourself, which is also loaded unpinned.
165
165
 
166
166
  ```bash
167
167
  # Install with all optional features
168
- pip install descry-codegraph[all]
168
+ pip install "descry-codegraph[all]"
169
169
 
170
170
  # Index your project
171
171
  cd your-project
@@ -225,6 +225,8 @@ Or with a specific Python path:
225
225
 
226
226
  Descry uses the standard MCP stdio transport. Any MCP-compatible host can spawn `descry-mcp` as a subprocess.
227
227
 
228
+ On Windows the executable is `descry-mcp.exe`. `python -m descry.mcp_server`, with the Python descry is installed in, starts the same server on every platform.
229
+
228
230
  ## Tools
229
231
 
230
232
  Descry provides 19 tools, available through all interfaces:
@@ -233,7 +235,7 @@ Descry provides 19 tools, available through all interfaces:
233
235
  |------|-------------|
234
236
  | `health` | Diagnostic check — version, graph status, feature availability |
235
237
  | `status` | Graph existence and freshness |
236
- | `ensure` | Ensure graph exists and is fresh (regenerates if stale) |
238
+ | `ensure` | Ensure graph exists and is fresh (regenerates if stale or files changed) |
237
239
  | `index` | Regenerate graph, SCIP indices, and embeddings |
238
240
  | `search` | Search symbol names and docstrings (keyword + semantic) |
239
241
  | `semantic` | Pure semantic search using embeddings only |
@@ -410,19 +412,19 @@ pip install descry-codegraph
410
412
  ### With MCP server
411
413
 
412
414
  ```bash
413
- pip install descry-codegraph[mcp]
415
+ pip install "descry-codegraph[mcp]"
414
416
  ```
415
417
 
416
418
  ### With Web UI
417
419
 
418
420
  ```bash
419
- pip install descry-codegraph[web]
421
+ pip install "descry-codegraph[web]"
420
422
  ```
421
423
 
422
424
  ### With semantic search
423
425
 
424
426
  ```bash
425
- pip install descry-codegraph[embeddings]
427
+ pip install "descry-codegraph[embeddings]"
426
428
  ```
427
429
 
428
430
  To select `embeddinggemma-2`, install its extra instead (adds Pillow and
@@ -435,7 +437,7 @@ pip install "descry-codegraph[embeddinggemma-2]"
435
437
  ### Everything
436
438
 
437
439
  ```bash
438
- pip install descry-codegraph[all]
440
+ pip install "descry-codegraph[all]"
439
441
  ```
440
442
 
441
443
  ### Choosing a torch build
@@ -489,7 +491,7 @@ Requires [uv](https://github.com/astral-sh/uv) and [just](https://github.com/cas
489
491
 
490
492
  3. **Query** — All tools query the cached graph. Keyword search uses TF-IDF scoring. Semantic search uses sentence-transformer embeddings. Call-graph traversal follows edges in the graph.
491
493
 
492
- 4. **Freshness** — `ensure` regenerates the graph when its age exceeds `[project] max_stale_hours` (default 24), or the `--max-age-hours` flag (CLI) / `max_age_hours` argument (MCP) when one is given. The same setting drives the "STALE" badge in `descry status`. The MCP server pre-warms the graph on startup.
494
+ 4. **Freshness** — `ensure` regenerates the graph when files it indexed changed or were removed since it was written, and when its age exceeds `[project] max_stale_hours` (default 24), or the `--max-age-hours` flag (CLI) / `max_age_hours` argument (MCP) when one is given. The same setting drives the "STALE" badge in `descry status`. The MCP server pre-warms the graph on startup.
493
495
 
494
496
  ## Design Notes
495
497
 
@@ -104,7 +104,7 @@ yourself, which is also loaded unpinned.
104
104
 
105
105
  ```bash
106
106
  # Install with all optional features
107
- pip install descry-codegraph[all]
107
+ pip install "descry-codegraph[all]"
108
108
 
109
109
  # Index your project
110
110
  cd your-project
@@ -164,6 +164,8 @@ Or with a specific Python path:
164
164
 
165
165
  Descry uses the standard MCP stdio transport. Any MCP-compatible host can spawn `descry-mcp` as a subprocess.
166
166
 
167
+ On Windows the executable is `descry-mcp.exe`. `python -m descry.mcp_server`, with the Python descry is installed in, starts the same server on every platform.
168
+
167
169
  ## Tools
168
170
 
169
171
  Descry provides 19 tools, available through all interfaces:
@@ -172,7 +174,7 @@ Descry provides 19 tools, available through all interfaces:
172
174
  |------|-------------|
173
175
  | `health` | Diagnostic check — version, graph status, feature availability |
174
176
  | `status` | Graph existence and freshness |
175
- | `ensure` | Ensure graph exists and is fresh (regenerates if stale) |
177
+ | `ensure` | Ensure graph exists and is fresh (regenerates if stale or files changed) |
176
178
  | `index` | Regenerate graph, SCIP indices, and embeddings |
177
179
  | `search` | Search symbol names and docstrings (keyword + semantic) |
178
180
  | `semantic` | Pure semantic search using embeddings only |
@@ -349,19 +351,19 @@ pip install descry-codegraph
349
351
  ### With MCP server
350
352
 
351
353
  ```bash
352
- pip install descry-codegraph[mcp]
354
+ pip install "descry-codegraph[mcp]"
353
355
  ```
354
356
 
355
357
  ### With Web UI
356
358
 
357
359
  ```bash
358
- pip install descry-codegraph[web]
360
+ pip install "descry-codegraph[web]"
359
361
  ```
360
362
 
361
363
  ### With semantic search
362
364
 
363
365
  ```bash
364
- pip install descry-codegraph[embeddings]
366
+ pip install "descry-codegraph[embeddings]"
365
367
  ```
366
368
 
367
369
  To select `embeddinggemma-2`, install its extra instead (adds Pillow and
@@ -374,7 +376,7 @@ pip install "descry-codegraph[embeddinggemma-2]"
374
376
  ### Everything
375
377
 
376
378
  ```bash
377
- pip install descry-codegraph[all]
379
+ pip install "descry-codegraph[all]"
378
380
  ```
379
381
 
380
382
  ### Choosing a torch build
@@ -428,7 +430,7 @@ Requires [uv](https://github.com/astral-sh/uv) and [just](https://github.com/cas
428
430
 
429
431
  3. **Query** — All tools query the cached graph. Keyword search uses TF-IDF scoring. Semantic search uses sentence-transformer embeddings. Call-graph traversal follows edges in the graph.
430
432
 
431
- 4. **Freshness** — `ensure` regenerates the graph when its age exceeds `[project] max_stale_hours` (default 24), or the `--max-age-hours` flag (CLI) / `max_age_hours` argument (MCP) when one is given. The same setting drives the "STALE" badge in `descry status`. The MCP server pre-warms the graph on startup.
433
+ 4. **Freshness** — `ensure` regenerates the graph when files it indexed changed or were removed since it was written, and when its age exceeds `[project] max_stale_hours` (default 24), or the `--max-age-hours` flag (CLI) / `max_age_hours` argument (MCP) when one is given. The same setting drives the "STALE" badge in `descry status`. The MCP server pre-warms the graph on startup.
432
434
 
433
435
  ## Design Notes
434
436
 
@@ -29,7 +29,7 @@ without forcing it on simple file-read tasks.
29
29
  ## Prerequisites
30
30
 
31
31
  - Python 3.11+.
32
- - `descry-codegraph` installed: `pip install descry-codegraph[all]`.
32
+ - `descry-codegraph` installed: `pip install "descry-codegraph[all]"`.
33
33
  - Optional but recommended: the MCP server registered (see below).
34
34
  - Optional: `rust-analyzer` (Rust SCIP), `scip-typescript` (TS SCIP),
35
35
  `scip-python` (Python SCIP) for type-aware call resolution.
@@ -83,7 +83,7 @@ under the project root. Before the first query in a session:
83
83
 
84
84
  ```bash
85
85
  descry status # Is there a graph? How old is it?
86
- descry ensure # Regenerate if missing or stale (respects max_stale_hours)
86
+ descry ensure # Regenerate if missing, stale (max_stale_hours) or files changed
87
87
  ```
88
88
 
89
89
  `descry ensure` is safe to call unconditionally — it no-ops if the graph is
@@ -92,12 +92,12 @@ refreshes SCIP indices and embeddings, which can take minutes on large
92
92
  Rust workspaces). If the user hasn't asked for indexing and no graph exists,
93
93
  prompt them before running `descry index`.
94
94
 
95
- Graph freshness is tracked by file mtime, and `descry ensure` rebuilds only
96
- a graph older than `[project] max_stale_hours` (24 by default). After the
97
- user edits code that changes structure (new symbols, renames), run
98
- `descry index` before querying again: `ensure` may report the graph ready
99
- while it still shows the old layout. Cached queriers, semantic searchers,
100
- and dedup caches all invalidate by mtime.
95
+ Graph freshness is tracked by file mtime: `descry ensure` rebuilds a graph
96
+ older than `[project] max_stale_hours` (24 by default), or one whose files
97
+ changed or were removed since it was written. A file added since is not
98
+ noticed: after the user adds files, run `descry index` before querying
99
+ again. Cached queriers, semantic searchers, and dedup caches all invalidate
100
+ by mtime.
101
101
 
102
102
  ## Commands — grouped by intent
103
103
 
@@ -7,9 +7,10 @@ the detected project root. Freshness is managed via `descry status` /
7
7
  ## Global discovery
8
8
 
9
9
  ```bash
10
+ descry --version # installed version
10
11
  descry health # version, graph status, SCIP/embeddings/git state
11
12
  descry status # just the graph: exists? nodes? edges? stale?
12
- descry ensure # regenerate only if missing or stale
13
+ descry ensure # regenerate only if missing, unusable, stale or files changed
13
14
  descry index [path] # force full rebuild (SCIP + embeddings)
14
15
  descry embedding-models # registered embedding models, and the one selected
15
16
  ```
@@ -23,7 +23,7 @@ This is descry's prompt-injection defense — preserve it when quoting.
23
23
  ### Freshness and diagnostics
24
24
  - `descry_health` — version, graph status, feature availability.
25
25
  - `descry_status` — graph existence / freshness only.
26
- - `descry_ensure` — regenerate iff missing or stale.
26
+ - `descry_ensure` — regenerate iff missing, unusable, stale or files it indexed changed.
27
27
  - `descry_index(path=".")` — authoritative rebuild. A `path` below the
28
28
  project root narrows which files are walked; the result still replaces
29
29
  the root's graph. Paths outside the root are rejected.
@@ -140,9 +140,9 @@ Requires an OpenAPI spec: set `[cross_lang] openapi_path = "..."` in
140
140
  - **Don't** use `descry search` for behavior-worded queries; `descry semantic`
141
141
  is built for that and will do better.
142
142
  - **Don't** bypass `status` on a stale graph. Out-of-date graphs
143
- produce plausible-looking wrong answers. After edits that change
144
- structure, `descry index` (not `ensure`, which rebuilds only a graph
145
- older than `max_stale_hours`) brings the graph current.
143
+ produce plausible-looking wrong answers. After edits, `descry ensure`
144
+ rebuilds a graph whose files changed or were removed; after adding
145
+ files, `descry index` brings the graph current.
146
146
  - **Don't** shell out to grep when the question is structural. Even one
147
147
  descry call beats three Greps for "who calls X".
148
148
 
@@ -54,8 +54,9 @@ answers; a call dropped as one out of the project is not among them.
54
54
 
55
55
  ## Keeping up with the source
56
56
 
57
- An index is a snapshot. `status` says how old it is; `ensure` builds it
58
- again when it is older than `[project] max_stale_hours`. An index after a
59
- change parses only the files that changed, and resolves again only the
60
- calls of files whose resolution read something that changed, so its
57
+ An index is a snapshot. `status` says how old it is and how many of its
58
+ files changed since; `ensure` builds it again when it is older than
59
+ `[project] max_stale_hours` or files it holds changed or were removed. An
60
+ index after a change parses only the files that changed, and resolves again
61
+ only the calls of files whose resolution read something that changed, so its
61
62
  answers are those a full index would give.
@@ -30,7 +30,7 @@ MCP `descry_health`; web `GET /api/health`; CLI `usage: descry health [--json]`
30
30
 
31
31
  ## ensure
32
32
 
33
- Ensure the codebase graph exists and is fresh. Call this FIRST before other descry queries. Regenerates if missing or older than max_age_hours (default: the project's configured max_stale_hours). Returns graph status with node/edge counts. WARNING: May take 30-60 seconds if regeneration is needed.
33
+ Ensure the codebase graph exists and is fresh. Call this FIRST before other descry queries. Regenerates if missing, older than max_age_hours (default: the project's configured max_stale_hours), or if files it indexed changed since. Returns graph status with node/edge counts. WARNING: May take 30-60 seconds if regeneration is needed.
34
34
 
35
35
  | parameter | type | default |
36
36
  |---|---|---|
@@ -72,7 +72,7 @@ export default function (pi: ExtensionAPI) {
72
72
  label: "Descry Ensure",
73
73
  description:
74
74
  "Ensure the codebase graph exists and is fresh. Call this FIRST before other descry queries. " +
75
- "Regenerates if missing or older than max_age_hours. Returns graph status with node/edge counts. " +
75
+ "Regenerates if missing, older than max_age_hours, or if files it indexed changed since. Returns graph status with node/edge counts. " +
76
76
  "WARNING: May take 30-60 seconds if regeneration is needed.",
77
77
  parameters: Type.Object({
78
78
  max_age_hours: Type.Optional(Type.Number({ description: "Max graph age in hours before regeneration (default: the project's max_stale_hours)" })),
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "descry-codegraph"
3
- version = "1.1.0"
3
+ dynamic = ["version"]
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"}]
@@ -72,5 +72,8 @@ extend-exclude = ["src/descry/scip/pb2.py"]
72
72
  requires = ["hatchling>=1.27"]
73
73
  build-backend = "hatchling.build"
74
74
 
75
+ [tool.hatch.version]
76
+ path = "src/descry/__init__.py"
77
+
75
78
  [tool.hatch.build.targets.wheel]
76
79
  packages = ["src/descry"]
@@ -1,4 +1,4 @@
1
1
  """Descry — Codebase knowledge graph: polyglot indexer, semantic search, call-graph analysis."""
2
2
 
3
- __version__ = "1.1.0"
3
+ __version__ = "1.2.1"
4
4
  __all__ = ["__version__"]
@@ -0,0 +1,158 @@
1
+ """Writing files into a directory the indexed repository may have shipped."""
2
+
3
+ import contextlib
4
+ import fnmatch
5
+ import glob
6
+ import os
7
+ import re
8
+ import secrets
9
+ import stat
10
+ import time
11
+ from pathlib import Path
12
+
13
+ try:
14
+ import fcntl as _fcntl # Unix only
15
+ except ImportError:
16
+ _fcntl = None
17
+
18
+ # A temporary nothing has written to for this long belongs to a writer that
19
+ # was killed before it could replace its target or remove the temporary.
20
+ _ORPHANED_AFTER = 3600
21
+
22
+
23
+ def create_sibling(path: Path) -> tuple[int, Path]:
24
+ """Create a new file beside `path` to write it atomically, then replace it.
25
+
26
+ The name is random and the file is created with `O_EXCL`, so neither a
27
+ leftover nor a symlink a repository put at a name it could guess is
28
+ opened or followed. The mode is the one `open(path, "w")` gives.
29
+ Temporaries for `path` that are orphaned are removed first.
30
+ Returns the open descriptor and the file's path.
31
+ """
32
+ remove_orphans(path.parent, glob.escape(path.name))
33
+ tmp = path.with_name(f".{path.name}.{secrets.token_hex(8)}.tmp")
34
+ flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0)
35
+ return os.open(tmp, flags, 0o666), tmp
36
+
37
+
38
+ def open_regular(path: Path, mode: str = "rb", **kwargs):
39
+ """`path` opened for reading if it is a regular file: never through a
40
+ symlink at its name, nor a FIFO or a device, which a repository can ship
41
+ where descry keeps files and a read of which would never end. OSError
42
+ for anything else."""
43
+ return os.fdopen(_open_regular_fd(path, os.O_RDONLY), mode, **kwargs)
44
+
45
+
46
+ def _open_regular_fd(path: Path, flags: int) -> int:
47
+ """`os.open(path, flags)` if the file is regular: never through a symlink
48
+ at its name, nor a FIFO or a device. OSError for anything else."""
49
+ flags |= getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
50
+ # Without O_NOFOLLOW (Windows), the name must be a regular file, or
51
+ # missing, before the open and the very file opened after it.
52
+ named = None
53
+ if not hasattr(os, "O_NOFOLLOW"):
54
+ with contextlib.suppress(FileNotFoundError):
55
+ named = os.lstat(path)
56
+ if named and not stat.S_ISREG(named.st_mode):
57
+ raise OSError(f"{path} is not a regular file")
58
+ fd = os.open(path, flags, 0o666)
59
+ opened = os.fstat(fd)
60
+ if not stat.S_ISREG(opened.st_mode) or (
61
+ named and not os.path.samestat(named, opened)
62
+ ):
63
+ os.close(fd)
64
+ raise OSError(f"{path} is not a regular file")
65
+ return fd
66
+
67
+
68
+ def remove_orphans(directory: Path, names: str) -> None:
69
+ """Remove the temporaries `create_sibling` made in `directory` for targets
70
+ whose names match the glob `names`, that nothing has written to for
71
+ `_ORPHANED_AFTER` seconds."""
72
+ cutoff = time.time() - _ORPHANED_AFTER
73
+ for tmp in directory.glob(f".{names}.*.tmp"):
74
+ target, _, token = tmp.name[1:-4].rpartition(".")
75
+ try:
76
+ st = tmp.lstat()
77
+ if (
78
+ re.fullmatch(r"[0-9a-f]{16}", token)
79
+ and fnmatch.fnmatchcase(target, names)
80
+ and not stat.S_ISDIR(st.st_mode)
81
+ and st.st_mtime < cutoff
82
+ ):
83
+ tmp.unlink()
84
+ except OSError:
85
+ continue
86
+
87
+
88
+ @contextlib.contextmanager
89
+ def file_lock(lock_path: Path, timeout: float = 600.0, create: bool = True):
90
+ """Cross-process advisory lock; with `create` False a missing lock file
91
+ raises FileNotFoundError instead of being created.
92
+
93
+ Uses fcntl.flock on Unix and msvcrt.locking on Windows. Both are held on
94
+ an open handle, so the OS releases them when the holder exits.
95
+
96
+ The lock file is never unlinked. Removing it would break mutual
97
+ exclusion: a waiter blocked on the old inode and a newcomer that
98
+ recreates the path hold locks on two different inodes and both enter.
99
+ An empty lock file is cheap; correctness is not.
100
+
101
+ The cache directory may have come with the repository, so the lock file
102
+ is opened without following a symlink or waiting on a FIFO, never
103
+ truncated, and refused unless it is a regular file.
104
+ """
105
+ if _fcntl is not None:
106
+ poll_seconds = 0.05
107
+
108
+ def lock(f):
109
+ _fcntl.flock(f, _fcntl.LOCK_EX | _fcntl.LOCK_NB)
110
+
111
+ def unlock(f):
112
+ _fcntl.flock(f, _fcntl.LOCK_UN)
113
+
114
+ else:
115
+ # Windows fallback: lock the first byte of the (always empty) lock file.
116
+ import msvcrt
117
+
118
+ poll_seconds = 0.1
119
+
120
+ def lock(f):
121
+ msvcrt.locking(f.fileno(), msvcrt.LK_NBLCK, 1)
122
+
123
+ def unlock(f):
124
+ msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
125
+
126
+ fd = _open_regular_fd(
127
+ lock_path, os.O_WRONLY | os.O_APPEND | (os.O_CREAT if create else 0)
128
+ )
129
+ with os.fdopen(fd, "a", encoding="utf-8") as f:
130
+ # Poll non-blocking so the timeout is honoured; a blocking lock would
131
+ # wait forever regardless of the argument.
132
+ deadline = time.monotonic() + timeout
133
+ while True:
134
+ try:
135
+ lock(f)
136
+ break
137
+ except OSError:
138
+ if time.monotonic() >= deadline:
139
+ raise TimeoutError(
140
+ f"Timed out waiting for the lock at {lock_path}"
141
+ ) from None
142
+ time.sleep(poll_seconds)
143
+ try:
144
+ yield
145
+ finally:
146
+ unlock(f)
147
+
148
+
149
+ def lock_held(lock_path: Path) -> bool:
150
+ """Whether another holder has `lock_path`'s `file_lock`; False where
151
+ the lock file is missing or cannot be opened, which it never creates."""
152
+ try:
153
+ with file_lock(lock_path, timeout=0, create=False):
154
+ return False
155
+ except TimeoutError:
156
+ return True
157
+ except OSError:
158
+ return False
@@ -14,6 +14,7 @@ import json
14
14
  import os
15
15
  import sys
16
16
 
17
+ from descry import __version__
17
18
  from descry._answers import fail
18
19
  from descry._streams import write_utf8
19
20
  from descry.handlers import (
@@ -249,6 +250,7 @@ def build_parser():
249
250
  prog="descry",
250
251
  description="Codebase knowledge graph tools",
251
252
  )
253
+ parser.add_argument("--version", action="version", version=f"descry {__version__}")
252
254
  subparsers = parser.add_subparsers(dest="command")
253
255
  answers = argparse.ArgumentParser(add_help=False)
254
256
  answers.add_argument(