coderadar-rs 0.7.0__tar.gz → 0.7.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/Cargo.lock +1 -1
  2. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/Cargo.toml +1 -1
  3. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/PKG-INFO +38 -9
  4. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/README.md +37 -8
  5. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/lib.rs +11 -0
  6. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/__init__.py +5 -0
  7. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/cli.py +18 -5
  8. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/mcp/lazy.py +14 -0
  9. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/mcp/roots.py +28 -0
  10. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/mcp/server.py +180 -30
  11. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/query/cache.py +6 -2
  12. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/pyproject.toml +1 -1
  13. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/LICENSE +0 -0
  14. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/Cargo.toml +0 -0
  15. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/bash.scm +0 -0
  16. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/c.scm +0 -0
  17. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/clojure.scm +0 -0
  18. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/cmake.scm +0 -0
  19. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/cpp.scm +0 -0
  20. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/csharp.scm +0 -0
  21. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/dart.scm +0 -0
  22. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/dockerfile.scm +0 -0
  23. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/elisp.scm +0 -0
  24. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/elixir.scm +0 -0
  25. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/erlang.scm +0 -0
  26. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/fsharp.scm +0 -0
  27. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/go.scm +0 -0
  28. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/graphql.scm +0 -0
  29. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/groovy.scm +0 -0
  30. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/haskell.scm +0 -0
  31. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/hcl.scm +0 -0
  32. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/java.scm +0 -0
  33. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/javascript.scm +0 -0
  34. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/julia.scm +0 -0
  35. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/kotlin.scm +0 -0
  36. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/lua.scm +0 -0
  37. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/nix.scm +0 -0
  38. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/objc.scm +0 -0
  39. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/ocaml.scm +0 -0
  40. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/perl.scm +0 -0
  41. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/php.scm +0 -0
  42. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/powershell.scm +0 -0
  43. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/protobuf.scm +0 -0
  44. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/python.scm +0 -0
  45. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/r.scm +0 -0
  46. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/ruby.scm +0 -0
  47. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/rust.scm +0 -0
  48. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/scala.scm +0 -0
  49. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/shell.scm +0 -0
  50. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/sql.scm +0 -0
  51. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/swift.scm +0 -0
  52. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/systemverilog.scm +0 -0
  53. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/typescript.scm +0 -0
  54. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/verilog.scm +0 -0
  55. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/queries/zig.scm +0 -0
  56. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/extract/decorators.rs +0 -0
  57. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/extract/docstring.rs +0 -0
  58. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/extract/mod.rs +0 -0
  59. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/extract/single_pass.rs +0 -0
  60. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/extract/spans.rs +0 -0
  61. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/extract/tagger.rs +0 -0
  62. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/extract/walker.rs +0 -0
  63. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/fs/git.rs +0 -0
  64. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/fs/mod.rs +0 -0
  65. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/fs/watcher.rs +0 -0
  66. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/call_graph.rs +0 -0
  67. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/config.rs +0 -0
  68. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/embeddings.rs +0 -0
  69. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/import_graph.rs +0 -0
  70. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/indexing.rs +0 -0
  71. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/inheritance.rs +0 -0
  72. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/mod.rs +0 -0
  73. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/module_resolution.rs +0 -0
  74. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/mro.rs +0 -0
  75. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/persistence.rs +0 -0
  76. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/projection_ops.rs +0 -0
  77. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/resolve_calls.rs +0 -0
  78. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/tests/call_graph_tests.rs +0 -0
  79. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/tests/embedding_tests.rs +0 -0
  80. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/tests/import_graph_tests.rs +0 -0
  81. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/tests/indexing_tests.rs +0 -0
  82. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/tests/inheritance_tests.rs +0 -0
  83. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/tests/mod.rs +0 -0
  84. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/tests/mro_tests.rs +0 -0
  85. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/tests/persistence_tests.rs +0 -0
  86. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/tests/projection_tests.rs +0 -0
  87. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/tests/query_compile_tests.rs +0 -0
  88. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/tests/traversal_tests.rs +0 -0
  89. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/graph/traversal.rs +0 -0
  90. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/mutation/edit.rs +0 -0
  91. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/mutation/indent.rs +0 -0
  92. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/mutation/mod.rs +0 -0
  93. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/mutation/write_guard.rs +0 -0
  94. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/query/exec.rs +0 -0
  95. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/query/grammar.pest +0 -0
  96. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/query/grammar.rs +0 -0
  97. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/query/mod.rs +0 -0
  98. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/resolve/cache.rs +0 -0
  99. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/resolve/import_graph.rs +0 -0
  100. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/resolve/mod.rs +0 -0
  101. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/resolve/orchestrator.rs +0 -0
  102. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/resolve/signature.rs +0 -0
  103. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/engine.rs +0 -0
  104. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/metrics.rs +0 -0
  105. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/mod.rs +0 -0
  106. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/rule.rs +0 -0
  107. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/rules/brain_method.rs +0 -0
  108. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/rules/data_class.rs +0 -0
  109. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/rules/deep_nesting.rs +0 -0
  110. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/rules/excessive_returns.rs +0 -0
  111. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/rules/god_class.rs +0 -0
  112. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/rules/high_cyclomatic_complexity.rs +0 -0
  113. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/rules/long_method.rs +0 -0
  114. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/rules/long_parameter_list.rs +0 -0
  115. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/rules/mod.rs +0 -0
  116. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/rules/too_many_fields.rs +0 -0
  117. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/smells/types.rs +0 -0
  118. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/storage.rs +0 -0
  119. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/types.rs +0 -0
  120. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/core_indexer/src/update/wal.rs +0 -0
  121. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/agent/__init__.py +0 -0
  122. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/agent/graphrag.py +0 -0
  123. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/config.py +0 -0
  124. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/embedding/__init__.py +0 -0
  125. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/embedding/dedup.py +0 -0
  126. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/lsp/__init__.py +0 -0
  127. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/lsp/pool.py +0 -0
  128. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/mcp/__init__.py +0 -0
  129. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/mcp/lifecycle.py +0 -0
  130. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/mcp/startup.py +0 -0
  131. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/mutation/__init__.py +0 -0
  132. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/mutation/tool_router.py +0 -0
  133. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/query/__init__.py +0 -0
  134. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/query/executor.py +0 -0
  135. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/query/planner.py +0 -0
  136. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/__init__.py +0 -0
  137. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/actix.py +0 -0
  138. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/aspnet.py +0 -0
  139. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/base.py +0 -0
  140. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/django.py +0 -0
  141. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/exports.py +0 -0
  142. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/express.py +0 -0
  143. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/fastapi.py +0 -0
  144. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/flask.py +0 -0
  145. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/go.py +0 -0
  146. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/laravel.py +0 -0
  147. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/nestjs.py +0 -0
  148. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/rails.py +0 -0
  149. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/reactrouter.py +0 -0
  150. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/resolution.py +0 -0
  151. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/springboot.py +0 -0
  152. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/resolvers/vuerouter.py +0 -0
  153. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/visualizers/__init__.py +0 -0
  154. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/visualizers/call_graph.py +0 -0
  155. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/visualizers/graphviz_viz.py +0 -0
  156. {coderadar_rs-0.7.0 → coderadar_rs-0.7.2}/py_agent/src/coderadar/visualizers/mermaid.py +0 -0
@@ -445,7 +445,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
445
445
 
446
446
  [[package]]
447
447
  name = "core_indexer"
448
- version = "0.7.0"
448
+ version = "0.7.2"
449
449
  dependencies = [
450
450
  "crossbeam-channel",
451
451
  "dashmap",
@@ -3,6 +3,6 @@ members = ["core_indexer"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "0.7.0"
6
+ version = "0.7.2"
7
7
  edition = "2021"
8
8
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderadar-rs
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -32,7 +32,7 @@ License: MIT
32
32
  Requires-Python: >=3.10
33
33
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
34
34
 
35
- # CodeRadar v0.7.0
35
+ # CodeRadar v0.7.2
36
36
 
37
37
  [![CI](https://github.com/opticsWolf/coderadar/actions/workflows/ci.yml/badge.svg)](https://github.com/opticsWolf/coderadar/actions/workflows/ci.yml)
38
38
  [![PyPI](https://img.shields.io/pypi/v/coderadar-rs?label=pypi)](https://pypi.org/project/coderadar-rs/)
@@ -88,8 +88,8 @@ Rust Core (ProjectedGraph, Tree-sitter 41-lang, Parallel Extraction,
88
88
  | Metric | Value |
89
89
  |--------|-------|
90
90
  | **Languages indexed** | 41 (12 Tier 1, 29 Tier 2, 330+ Tier 3) |
91
- | **Tests** | 857 (250 Rust + 607 Python) |
92
- | **MCP Tools** | 18 (explore, search, node, affected, resolve, query, search_similar, module_children, as_of, traverse, get_smells, replace_body, update_signature, rename, create_entity, compute_embeddings, reindex, update_file) |
91
+ | **Tests** | 883 (250 Rust + 633 Python) |
92
+ | **MCP Tools** | 19 (explore, search, node, affected, resolve, query, search_similar, module_children, as_of, traverse, get_smells, replace_body, update_signature, rename, create_entity, compute_embeddings, reindex, update_file, set_project) |
93
93
  | **Query surface** | Pest structural + Macrame agent traversals + vector search |
94
94
  | **Frameworks** | Django, Flask, FastAPI, Go, Actix, Express, Spring Boot, Laravel, ASP.NET, Rails, NestJS, Vue Router, React Router |
95
95
  | **Agents** | MCP server over stdio — finds the project root, indexes in the background, and exits with its client |
@@ -209,9 +209,15 @@ not assume the cwd is the project:
209
209
  - **Saying where it looked.** A "no index" reply names the directory being
210
210
  served and how that directory was chosen, so an agent pointed at the wrong
211
211
  project can say so.
212
- - **One project per server.** Every tool takes an optional `project_path`; a
213
- path that is not the served root is refused with the reason, not quietly
214
- answered from the wrong codebase.
212
+ - **One project at a time.** Every tool takes an optional `project_path`; a path
213
+ inside the served project (a file, a subdirectory, or the root itself) is
214
+ accepted via nearest-marker resolution; another project is refused with the
215
+ reason, not quietly answered from the wrong codebase.
216
+ - **Switching projects without restarting.** `codegraph_set_project` re-runs
217
+ startup against a new root from inside a tool call: that project's config
218
+ and mutation policy take effect, indexing restarts in the background, and an
219
+ explicit switch outranks the client's declared workspace for the rest of the
220
+ connection.
215
221
  - **Not outliving the client.** Handshake timeout, parent-process watchdog, and
216
222
  teardown when stdin closes.
217
223
 
@@ -259,6 +265,29 @@ CodeRadar detects and extracts framework-specific patterns that tree-sitter can'
259
265
 
260
266
  Framework edges are registered in the Rust graph — agents can trace from URL patterns to handler functions via `callers_of()` / `callees_of()`.
261
267
 
268
+ ## v0.7.2 Feature Highlights
269
+
270
+ - **Runtime project switching** — the new `codegraph_set_project` MCP tool
271
+ (19 total) re-runs startup against a new root from inside a tool call:
272
+ that project's `.coderadar.toml` config and mutation policy take effect,
273
+ indexing restarts in the background, and an explicit switch outranks the
274
+ client's declared workspace for the rest of the connection. Unmarked roots
275
+ require `confirm=true`; switching to the current root is a no-op that says
276
+ so.
277
+ - **`project_path` reads like agents mean it** — a file, subdirectory, or
278
+ root inside the served project is accepted via nearest-marker walk-up
279
+ (`resolve_selector`), replacing a byte-exact root comparison that refused
280
+ editor-tab paths and explored directories. Windows drive-letter casing can
281
+ no longer split one directory in two.
282
+ - **Refusals name the way out** — "wrong project" replies point at
283
+ `codegraph_set_project` instead of telling the agent to edit mcp.json and
284
+ restart the server.
285
+ - **One writable project at a time, unchanged** — mutation confinement
286
+ follows the switched root automatically: `analyze(root)` re-tightens
287
+ `INDEXED_ROOT`, so policy, stale-write hashes and rollback need no
288
+ redesign. E2E tests prove an escape-path mutation into the previous
289
+ project never touches disk.
290
+
262
291
  ## v0.7.0 Feature Highlights
263
292
 
264
293
  The v0.7 improvement plan, start to finish — write-path correctness, temporal
@@ -456,7 +485,7 @@ py_agent/src/coderadar/ # Python layer
456
485
  lsp/ # Persistent LSP warm pool
457
486
  mutation/ # Tool router for LLM
458
487
  mcp/ # MCP server
459
- server.py # 18 tools + guidance
488
+ server.py # 19 tools + guidance
460
489
  roots.py # project-root ladder and marker walk-up
461
490
  startup.py # background index, ensure_ready()
462
491
  lazy.py # roots/list retry on the first tool call
@@ -465,7 +494,7 @@ py_agent/src/coderadar/ # Python layer
465
494
  visualizers/ # Mermaid + Graphviz (SCC cycle highlighting)
466
495
 
467
496
  docs/ # Specifications + code review + performance roadmap
468
- tests/ # 607 Python tests (E2E, mutation E2E, MCP, smells,
497
+ tests/ # 633 Python tests (E2E, mutation E2E, MCP, smells,
469
498
  # framework resolvers, ingest parity, benchmarks)
470
499
  mcp/ # Root resolution, background init, lifecycle, project_path
471
500
  ```
@@ -1,4 +1,4 @@
1
- # CodeRadar v0.7.0
1
+ # CodeRadar v0.7.2
2
2
 
3
3
  [![CI](https://github.com/opticsWolf/coderadar/actions/workflows/ci.yml/badge.svg)](https://github.com/opticsWolf/coderadar/actions/workflows/ci.yml)
4
4
  [![PyPI](https://img.shields.io/pypi/v/coderadar-rs?label=pypi)](https://pypi.org/project/coderadar-rs/)
@@ -54,8 +54,8 @@ Rust Core (ProjectedGraph, Tree-sitter 41-lang, Parallel Extraction,
54
54
  | Metric | Value |
55
55
  |--------|-------|
56
56
  | **Languages indexed** | 41 (12 Tier 1, 29 Tier 2, 330+ Tier 3) |
57
- | **Tests** | 857 (250 Rust + 607 Python) |
58
- | **MCP Tools** | 18 (explore, search, node, affected, resolve, query, search_similar, module_children, as_of, traverse, get_smells, replace_body, update_signature, rename, create_entity, compute_embeddings, reindex, update_file) |
57
+ | **Tests** | 883 (250 Rust + 633 Python) |
58
+ | **MCP Tools** | 19 (explore, search, node, affected, resolve, query, search_similar, module_children, as_of, traverse, get_smells, replace_body, update_signature, rename, create_entity, compute_embeddings, reindex, update_file, set_project) |
59
59
  | **Query surface** | Pest structural + Macrame agent traversals + vector search |
60
60
  | **Frameworks** | Django, Flask, FastAPI, Go, Actix, Express, Spring Boot, Laravel, ASP.NET, Rails, NestJS, Vue Router, React Router |
61
61
  | **Agents** | MCP server over stdio — finds the project root, indexes in the background, and exits with its client |
@@ -175,9 +175,15 @@ not assume the cwd is the project:
175
175
  - **Saying where it looked.** A "no index" reply names the directory being
176
176
  served and how that directory was chosen, so an agent pointed at the wrong
177
177
  project can say so.
178
- - **One project per server.** Every tool takes an optional `project_path`; a
179
- path that is not the served root is refused with the reason, not quietly
180
- answered from the wrong codebase.
178
+ - **One project at a time.** Every tool takes an optional `project_path`; a path
179
+ inside the served project (a file, a subdirectory, or the root itself) is
180
+ accepted via nearest-marker resolution; another project is refused with the
181
+ reason, not quietly answered from the wrong codebase.
182
+ - **Switching projects without restarting.** `codegraph_set_project` re-runs
183
+ startup against a new root from inside a tool call: that project's config
184
+ and mutation policy take effect, indexing restarts in the background, and an
185
+ explicit switch outranks the client's declared workspace for the rest of the
186
+ connection.
181
187
  - **Not outliving the client.** Handshake timeout, parent-process watchdog, and
182
188
  teardown when stdin closes.
183
189
 
@@ -225,6 +231,29 @@ CodeRadar detects and extracts framework-specific patterns that tree-sitter can'
225
231
 
226
232
  Framework edges are registered in the Rust graph — agents can trace from URL patterns to handler functions via `callers_of()` / `callees_of()`.
227
233
 
234
+ ## v0.7.2 Feature Highlights
235
+
236
+ - **Runtime project switching** — the new `codegraph_set_project` MCP tool
237
+ (19 total) re-runs startup against a new root from inside a tool call:
238
+ that project's `.coderadar.toml` config and mutation policy take effect,
239
+ indexing restarts in the background, and an explicit switch outranks the
240
+ client's declared workspace for the rest of the connection. Unmarked roots
241
+ require `confirm=true`; switching to the current root is a no-op that says
242
+ so.
243
+ - **`project_path` reads like agents mean it** — a file, subdirectory, or
244
+ root inside the served project is accepted via nearest-marker walk-up
245
+ (`resolve_selector`), replacing a byte-exact root comparison that refused
246
+ editor-tab paths and explored directories. Windows drive-letter casing can
247
+ no longer split one directory in two.
248
+ - **Refusals name the way out** — "wrong project" replies point at
249
+ `codegraph_set_project` instead of telling the agent to edit mcp.json and
250
+ restart the server.
251
+ - **One writable project at a time, unchanged** — mutation confinement
252
+ follows the switched root automatically: `analyze(root)` re-tightens
253
+ `INDEXED_ROOT`, so policy, stale-write hashes and rollback need no
254
+ redesign. E2E tests prove an escape-path mutation into the previous
255
+ project never touches disk.
256
+
228
257
  ## v0.7.0 Feature Highlights
229
258
 
230
259
  The v0.7 improvement plan, start to finish — write-path correctness, temporal
@@ -422,7 +451,7 @@ py_agent/src/coderadar/ # Python layer
422
451
  lsp/ # Persistent LSP warm pool
423
452
  mutation/ # Tool router for LLM
424
453
  mcp/ # MCP server
425
- server.py # 18 tools + guidance
454
+ server.py # 19 tools + guidance
426
455
  roots.py # project-root ladder and marker walk-up
427
456
  startup.py # background index, ensure_ready()
428
457
  lazy.py # roots/list retry on the first tool call
@@ -431,7 +460,7 @@ py_agent/src/coderadar/ # Python layer
431
460
  visualizers/ # Mermaid + Graphviz (SCC cycle highlighting)
432
461
 
433
462
  docs/ # Specifications + code review + performance roadmap
434
- tests/ # 607 Python tests (E2E, mutation E2E, MCP, smells,
463
+ tests/ # 633 Python tests (E2E, mutation E2E, MCP, smells,
435
464
  # framework resolvers, ingest parity, benchmarks)
436
465
  mcp/ # Root resolution, background init, lifecycle, project_path
437
466
  ```
@@ -1580,6 +1580,17 @@ fn graph_stats(py: Python<'_>) -> PyResult<PyObject> {
1580
1580
  // Unix seconds of the last projection commit; 0.0 means never indexed.
1581
1581
  // The MCP staleness banner reads this.
1582
1582
  dict.set_item("indexed_at", graph.indexed_at())?;
1583
+ // Where the loaded graph was walked from. Callers compare it against
1584
+ // their own cwd: a graph belongs to one directory, and reusing dir
1585
+ // A's graph to answer about dir B is how stale, wrong-tree answers
1586
+ // look exactly like fresh ones.
1587
+ dict.set_item(
1588
+ "indexed_root",
1589
+ INDEXED_ROOT
1590
+ .read()
1591
+ .clone()
1592
+ .map(|p| p.to_string_lossy().into_owned()),
1593
+ )?;
1583
1594
  Ok(dict.into())
1584
1595
  })
1585
1596
  }
@@ -14,6 +14,11 @@ v3.6 Architecture:
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ #: The single version constant. pyproject.toml, Cargo.toml and this value
18
+ #: move together on a release; the CLI banner, the MCP handshake and the
19
+ #: tests read from here so no string is hardcoded twice.
20
+ __version__ = "0.7.2"
21
+
17
22
  import json
18
23
  from pathlib import Path
19
24
  from typing import Any, Dict, Iterator, List, Literal, Optional, Union
@@ -11,6 +11,8 @@ import click
11
11
  from rich.console import Console
12
12
  from rich.table import Table
13
13
 
14
+ from . import __version__
15
+
14
16
  console = Console()
15
17
 
16
18
 
@@ -125,25 +127,36 @@ def _ensure_graph(path: str = "."):
125
127
  — advice the user had just taken. Cross-process cold start from the
126
128
  Macrame ledger (`load_snapshot`) is Phase 3B; until then the honest
127
129
  behaviour is to index here rather than to send the user in a circle.
130
+
131
+ A loaded graph is only reused when it belongs to *this* directory: the
132
+ core records the root each index walked, and a graph from somewhere else
133
+ answers every question about this tree wrongly while looking completely
134
+ healthy. Different root — or no graph — means index here.
128
135
  """
129
136
  import coderadar
130
137
  from coderadar._core import graph_stats
131
138
 
132
139
  graph = coderadar.CodeGraph()
133
140
  try:
134
- graph_stats()
135
- return graph
141
+ stats = graph_stats()
142
+ root = stats.get("indexed_root")
143
+ if root:
144
+ # std::fs::canonicalize emits Windows verbatim paths (\\?\C:\…);
145
+ # pathlib compares them as a different directory, so strip first.
146
+ root_str = str(root).removeprefix("\\\\?\\")
147
+ if Path(root_str).resolve() == Path(path).resolve():
148
+ return graph
136
149
  except RuntimeError:
137
150
  pass
138
151
 
139
152
  _activate(path)
140
- console.print("[dim]No graph in this process — indexing first...[/dim]")
153
+ console.print("[dim]No graph for this directory — indexing...[/dim]")
141
154
  coderadar.analyze(path)
142
155
  return graph
143
156
 
144
157
 
145
158
  @click.group()
146
- @click.version_option(version="0.7.0", prog_name="coderadar",
159
+ @click.version_option(version=__version__, prog_name="coderadar",
147
160
  message="coderadar %(version)s (spec v3.6)")
148
161
  def main():
149
162
  """CodeRadar — live semantic graph of your codebase.
@@ -672,7 +685,7 @@ def serve(project_path: str | None):
672
685
  """Start the CodeRadar MCP server over stdio.
673
686
 
674
687
  Connect an MCP client (Claude Code, Cursor, etc.) to this server to get
675
- code intelligence over the indexed project — 18 tools covering search,
688
+ code intelligence over the indexed project — 19 tools covering search,
676
689
  exploration, structural and temporal queries, code smells, and the
677
690
  plan/review/apply mutation pipeline.
678
691
 
@@ -74,6 +74,20 @@ class LazyRootRetry:
74
74
 
75
75
  # ── action ───────────────────────────────────────────────────────────
76
76
 
77
+ def mark_user_chosen(self, resolved: ResolvedRoot) -> None:
78
+ """Record an explicit ``codegraph_set_project`` choice.
79
+
80
+ An explicit tool call is the strongest signal there is — stronger
81
+ than a marker found at startup, and much stronger than the client's
82
+ declared workspace. The retry is marked attempted so the lazy
83
+ roots/list dance can never fire again: an agent that named the
84
+ project it wants does not want middleware second-guessing that with
85
+ whatever the host happens to declare as its workspace root.
86
+ """
87
+ with self._lock:
88
+ self._attempted = True
89
+ self.resolved = resolved
90
+
77
91
  async def attempt(self, session: Any) -> bool:
78
92
  """Ask, re-resolve, and move if the answer is better. Returns whether
79
93
  anything changed."""
@@ -42,6 +42,8 @@ MARKERS = (".coderadar", ".coderadar.toml")
42
42
  CLIENT_ROOT = "client root"
43
43
  PATH_FLAG = "--path"
44
44
  CWD = "cwd"
45
+ #: Source name for a root named by a tool call's ``project_path`` argument.
46
+ PROJECT_PATH = "project_path"
45
47
 
46
48
 
47
49
  @dataclass(frozen=True)
@@ -220,6 +222,32 @@ def resolve_from_client_roots(uris: Iterable[str]) -> Optional[ResolvedRoot]:
220
222
  return None
221
223
 
222
224
 
225
+ def resolve_selector(project_path: str) -> Optional[ResolvedRoot]:
226
+ """Resolve a tool call's ``project_path`` argument the way agents mean it.
227
+
228
+ Agents rarely pass a project *root*: they pass a source file they can see
229
+ in an editor, a subdirectory they explored earlier, or a path with a
230
+ trailing component that only looks redundant. All of them mean "the
231
+ project this thing belongs to", which is exactly what the walk-up finds —
232
+ the same rule the startup ladder applies to its own candidates. A bare
233
+ directory with no marker anywhere above it is still answered, but with
234
+ ``marker=None`` so the caller says out loud that nothing confirmed it.
235
+
236
+ Returns None only when no part of the path names a usable directory —
237
+ the same rule ``_canonical`` applies for the ladder: a nonexistent name
238
+ under a real directory is read as a file of that directory, but a path
239
+ whose every component is missing is a caller error worth naming, not a
240
+ project to guess about.
241
+ """
242
+ resolved = _canonical(Path(project_path))
243
+ if resolved is None:
244
+ return None
245
+ marker = find_marker(resolved)
246
+ if marker is not None:
247
+ return ResolvedRoot(path=marker.parent, source=PROJECT_PATH, marker=marker)
248
+ return ResolvedRoot(path=resolved, source=PROJECT_PATH, marker=None)
249
+
250
+
223
251
  def adopt_project_root(resolved: ResolvedRoot) -> Path:
224
252
  """Make the resolved root the process cwd, and report the old one.
225
253
 
@@ -21,6 +21,7 @@ import functools
21
21
  import re
22
22
  import os
23
23
  from collections import deque
24
+ from pathlib import Path
24
25
  from typing import Any, Literal, Optional
25
26
 
26
27
  from mcp.server import MCPServer
@@ -115,15 +116,19 @@ After editing code, use the mutation pipeline to keep the graph in sync:
115
116
  - **When a file is flagged "⚠ changed on disk after index sync"**, Read those specific files for accurate content. Every file NOT flagged is fresh — still trust codegraph.
116
117
  - **If a project isn't indexed**, stop calling codegraph tools for that project and use built-in tools. Indexing is the user's decision — mention `coderadar init` if it comes up, but don't run it yourself.
117
118
 
118
- ## One project per server
119
+ ## One project at a time
119
120
 
120
- This server serves a single project — the one named in the "no index" and
121
- "wrong project" messages. Every tool takes an optional `project_path`; pass
122
- it when you are working across repositories and want to be told, rather than
123
- quietly answered from the wrong one. A `project_path` that names the served
124
- project is accepted; anything else is refused with the reason. To work on a
125
- second project, start a second server with `coderadar mcp serve --path
126
- <project root>`.
121
+ This server serves a single project at a time — the one named in the "no
122
+ index" and "wrong project" messages. Every tool takes an optional
123
+ `project_path`; pass it when you want to be told, rather than quietly
124
+ answered from the wrong one. A `project_path` inside the served project (a
125
+ file, a subdirectory, or the root itself) is accepted; another project is
126
+ refused with the reason.
127
+
128
+ To work on a different project, call `codegraph_set_project` with any path
129
+ inside it — this server switches there, re-reads that project's config and
130
+ re-indexes in the background. Switching is wholesale: after it, earlier
131
+ event ids from the previous project are no longer valid.
127
132
 
128
133
  If a tool reports that indexing is still in progress, that is not an error —
129
134
  the first index walks every source file. Retry in a few seconds rather than
@@ -140,9 +145,11 @@ def create_server(graph: Any) -> MCPServer:
140
145
  Every tool is a plain, type-hinted Python function; MCPServer derives
141
146
  JSON Schema and routing from the function signature.
142
147
  """
148
+ from coderadar import __version__
149
+
143
150
  mcp = MCPServer(
144
151
  "CodeRadar",
145
- version="0.7.0",
152
+ version=__version__,
146
153
  instructions=SERVER_INSTRUCTIONS,
147
154
  )
148
155
 
@@ -698,6 +705,31 @@ def create_server(graph: Any) -> MCPServer:
698
705
 
699
706
  return _update_file(graph, file_path, content)
700
707
 
708
+ # ── codegraph_set_project — switch the served project ───────────
709
+
710
+ @mcp.tool(
711
+ description=(
712
+ "Switch this server to a different project. All other tools then "
713
+ "verify their project_path against this root. Pass any directory, "
714
+ "subdirectory or file inside the target project — the nearest "
715
+ ".coderadar marker at or above it defines the root; pass "
716
+ "confirm=true only if the directory has no marker and you want it "
717
+ "served anyway. Config and mutation policy are re-read from the "
718
+ "new project, and indexing restarts in the background. Switching "
719
+ "replaces the graph wholesale: calls racing a switch get the old "
720
+ "or new answer, never a mix."
721
+ ),
722
+ annotations={
723
+ "read_only_hint": False,
724
+ "destructive_hint": False,
725
+ "idempotent_hint": False,
726
+ "open_world_hint": True,
727
+ },
728
+ )
729
+ def codegraph_set_project(project_path: str, confirm: bool = False) -> str:
730
+ """Switch the served project."""
731
+ return _set_project(project_path, confirm)
732
+
701
733
  return mcp
702
734
 
703
735
 
@@ -962,6 +994,19 @@ def _trim_to_char_budget(body_lines: list[str], max_chars: int) -> list[str]:
962
994
 
963
995
  # ── Tool Implementations ─────────────────────────────────────────────────
964
996
 
997
+ def _served_root() -> Path:
998
+ """The root this server currently serves.
999
+
1000
+ The lazy retry handle holds it once startup ran; a directly constructed
1001
+ server (tests, embedders) falls back to the process cwd, by the same
1002
+ reasoning that made serve chdir onto the root.
1003
+ """
1004
+ from coderadar.mcp import lazy
1005
+
1006
+ retry = lazy.current()
1007
+ return retry.resolved.path if retry is not None else Path.cwd().resolve()
1008
+
1009
+
965
1010
  def _wrong_project(project_path: Optional[str]) -> Optional[str]:
966
1011
  """Answer honestly when a tool is asked about a project we are not serving.
967
1012
 
@@ -970,39 +1015,44 @@ def _wrong_project(project_path: Optional[str]) -> Optional[str]:
970
1015
  question nobody asked — about the wrong codebase, with no sign that
971
1016
  anything went wrong.
972
1017
 
973
- This build cannot serve more than one project: the core keeps a single
974
- GLOBAL_GRAPH and `analyze` replaces it wholesale rather than merging. So
975
- a matching path is accepted and anything else is refused with the reason
976
- and the workaround, rather than quietly answered.
1018
+ This build serves one project at a time: the core keeps a single
1019
+ GLOBAL_GRAPH and `analyze` replaces it wholesale rather than merging. But
1020
+ the argument is read the way agents mean it: the selector walks up from
1021
+ whatever was passed — a file, a subdirectory, or the root itself — to the
1022
+ nearest `.coderadar` marker, so anything inside the served project is
1023
+ accepted. Only a path that lands in some other project is refused, with
1024
+ the reason and the way out rather than a quietly wrong answer.
977
1025
 
978
1026
  Returns None when the call may proceed, or the message to return instead.
979
1027
  """
980
1028
  if not project_path:
981
1029
  return None
982
1030
 
983
- from pathlib import Path
984
-
985
1031
  from coderadar.mcp import lazy
1032
+ from coderadar.mcp.roots import resolve_selector
986
1033
 
987
- try:
988
- asked = Path(project_path).expanduser().resolve()
989
- except (OSError, RuntimeError):
990
- asked = Path(project_path)
1034
+ selected = resolve_selector(project_path)
1035
+ if selected is None:
1036
+ return (
1037
+ f"`{project_path}` names no readable directory, so no project "
1038
+ "can be selected from it. Pass a directory (or any file inside "
1039
+ "one) and retry."
1040
+ )
991
1041
 
992
- retry = lazy.current()
993
- served = retry.resolved.path if retry is not None else Path.cwd().resolve()
994
- if asked == served:
1042
+ served = _served_root()
1043
+ # Windows paths keep their drive-letter casing through resolve(); compare
1044
+ # case-insensitively so D:/User and d:/user are one directory everywhere.
1045
+ if os.path.normcase(str(selected.path)) == os.path.normcase(str(served)):
995
1046
  return None
996
1047
 
997
1048
  return (
998
1049
  f"This server is serving `{served}` and cannot answer for "
999
- f"`{asked}`.\n\n"
1000
- "One project per server in this build: the index is a single "
1001
- "in-process graph, and pointing it elsewhere would replace the "
1002
- "project every other open tool call is about.\n\n"
1003
- "Start a second server for that project with "
1004
- "`coderadar mcp serve --path <project root>`, or drop the "
1005
- "`project_path` argument to ask about the one being served."
1050
+ f"`{selected.path}`.\n\n"
1051
+ "One project at a time in this build: the index is a single "
1052
+ "in-process graph. To ask about that other project instead, call "
1053
+ f"`codegraph_set_project` with `{selected.path}` — this server "
1054
+ "switches there and re-indexes. Or drop the `project_path` argument "
1055
+ f"to keep asking about `{served}`."
1006
1056
  )
1007
1057
 
1008
1058
 
@@ -1042,7 +1092,8 @@ def _no_index_message() -> str:
1042
1092
  "Nothing on disk confirmed that directory as a project root — no "
1043
1093
  "`.coderadar/` or `.coderadar.toml` was found at or above it.",
1044
1094
  "",
1045
- "If that is the wrong project, restart the server with "
1095
+ "If that is the wrong project, call `codegraph_set_project` with "
1096
+ "the right directory to switch this server there, restart with "
1046
1097
  "`coderadar mcp serve --path <project root>`, or run "
1047
1098
  "`coderadar init` in the right directory so it can be found "
1048
1099
  "automatically.",
@@ -1052,6 +1103,105 @@ def _no_index_message() -> str:
1052
1103
  return "\n".join(lines)
1053
1104
 
1054
1105
 
1106
+ def _set_project(project_path: str, confirm: bool = False) -> str:
1107
+ """Switch the served project, and say what happened.
1108
+
1109
+ One project at a time is the shape of the core — a single GLOBAL_GRAPH
1110
+ that `analyze` replaces wholesale — so switching means re-running the
1111
+ whole start-up sequence against the new root: config in, process moved,
1112
+ index restarted. Every step already exists from launch; this orders them
1113
+ for a tool call instead.
1114
+
1115
+ The order matters. Config first, so `[mutation]` policy and excludes are
1116
+ read from the *new* project before anything walks it; chdir second, so
1117
+ entity ids and every cwd-relative helper agree with the root; restart
1118
+ last, so the generation bump cannot race the config swap.
1119
+
1120
+ An explicit tool call outranks everything: the lazy roots/list retry is
1121
+ retired for the rest of the connection, because an agent that named the
1122
+ project it wants must not be second-guessed by whatever the host declares
1123
+ as its workspace.
1124
+ """
1125
+ from coderadar.config import activate_config
1126
+ from coderadar.mcp import lazy, startup
1127
+ from coderadar.mcp.roots import adopt_project_root, resolve_selector
1128
+
1129
+ selected = resolve_selector(project_path)
1130
+ if selected is None:
1131
+ return (
1132
+ f"`{project_path}` names no readable directory, so no project "
1133
+ "can be selected from it. Pass a directory (or any file inside "
1134
+ "one) and retry."
1135
+ )
1136
+
1137
+ if not selected.confirmed and not confirm:
1138
+ return (
1139
+ f"No `.coderadar/` or `.coderadar.toml` was found at or above "
1140
+ f"`{selected.path}`, so nothing confirms that directory as a "
1141
+ "project root.\n\n"
1142
+ "Run `coderadar init` there if it should be one, or re-call "
1143
+ "with confirm=true to serve it anyway (unmarked roots are "
1144
+ "served as bare guesses, same as at startup)."
1145
+ )
1146
+
1147
+ # Already there? Say so rather than silently re-indexing the same tree.
1148
+ served_now = _served_root()
1149
+ if os.path.normcase(str(selected.path)) == os.path.normcase(str(served_now)):
1150
+ return (
1151
+ f"Already serving `{selected.path}` — nothing to switch. Use "
1152
+ "`codegraph_reindex` to refresh the index."
1153
+ )
1154
+
1155
+ lines = [f"Switched to `{selected.path}`"]
1156
+ if selected.confirmed:
1157
+ lines[0] += f" (marker {selected.marker.name})"
1158
+ else:
1159
+ lines[0] += " — unconfirmed: no marker found, serving on your say-so"
1160
+
1161
+ # 1. Config from the NEW project, before anything reads it.
1162
+ try:
1163
+ activated = activate_config(selected.path)
1164
+ if activated.ignored:
1165
+ lines.append(
1166
+ f"Config: {len(activated.ignored)} setting(s) with no "
1167
+ f"consumer were ignored ({', '.join(activated.ignored[:3])}" +
1168
+ (", ..." if len(activated.ignored) > 3 else "") + ")."
1169
+ )
1170
+ except Exception as exc: # noqa: BLE001 — a broken config must not strand the server on the old project
1171
+ lines.append(
1172
+ f"WARNING: config not applied ({type(exc).__name__}: {exc}) — "
1173
+ "this project runs on defaults, including default mutation "
1174
+ "policy."
1175
+ )
1176
+ structlog.get_logger(__name__).warning(
1177
+ "mcp.set_project.config_failed", root=str(selected.path),
1178
+ error=str(exc))
1179
+
1180
+ # 2. Move the process: entity ids and cwd-relative helpers follow.
1181
+ adopt_project_root(selected)
1182
+
1183
+ # 3. Re-index in the background; ensure_ready makes callers wait or
1184
+ # report progress, never answer from the old graph.
1185
+ index = startup.current()
1186
+ if index is not None:
1187
+ index.restart(".")
1188
+ lines.append("Indexing restarted in the background — tool calls "
1189
+ "will wait for it or report progress.")
1190
+ else:
1191
+ lines.append("No background index handle in this process — call "
1192
+ "`codegraph_reindex` to build the new graph.")
1193
+
1194
+ # 4. The explicit choice outranks the client's workspace forever after.
1195
+ retry = lazy.current()
1196
+ if retry is not None:
1197
+ retry.mark_user_chosen(selected)
1198
+
1199
+ structlog.get_logger(__name__).info(
1200
+ "mcp.project.switched", to=str(selected.path),
1201
+ confirmed=selected.confirmed)
1202
+ return "\n".join(lines)
1203
+
1204
+
1055
1205
  NO_EXTENSION_MESSAGE = (
1056
1206
  "The CodeRadar native extension is not available in this environment, so "
1057
1207
  "no code intelligence can be served.\n\n"
@@ -29,7 +29,10 @@ class QueryCache:
29
29
  entry = self._cache.get(key)
30
30
  if entry is None:
31
31
  return None
32
- if time.monotonic() - entry.timestamp > self._ttl_seconds:
32
+ # >=, not >: a ttl of 0 means "never valid", and on Windows two
33
+ # monotonic() reads around a set() can be equal — a strict compare
34
+ # made ttl=0 entries immortal there while Linux timers masked it.
35
+ if time.monotonic() - entry.timestamp >= self._ttl_seconds:
33
36
  del self._cache[key]
34
37
  return None
35
38
  return entry.value
@@ -45,9 +48,10 @@ class QueryCache:
45
48
 
46
49
  def prune_expired(self) -> int:
47
50
  now = time.monotonic()
51
+ # Same >= as get(): ttl=0 expires everything, deterministically.
48
52
  expired = [
49
53
  k for k, v in self._cache.items()
50
- if now - v.timestamp > self._ttl_seconds
54
+ if now - v.timestamp >= self._ttl_seconds
51
55
  ]
52
56
  for k in expired:
53
57
  del self._cache[k]