codecortex 0.23.3__tar.gz → 0.24.0__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 (173) hide show
  1. {codecortex-0.23.3/src/codecortex.egg-info → codecortex-0.24.0}/PKG-INFO +8 -3
  2. {codecortex-0.23.3 → codecortex-0.24.0}/README.md +6 -2
  3. {codecortex-0.23.3 → codecortex-0.24.0}/pyproject.toml +33 -1
  4. {codecortex-0.23.3 → codecortex-0.24.0/src/codecortex.egg-info}/PKG-INFO +8 -3
  5. {codecortex-0.23.3 → codecortex-0.24.0}/src/codecortex.egg-info/SOURCES.txt +19 -0
  6. {codecortex-0.23.3 → codecortex-0.24.0}/src/codecortex.egg-info/requires.txt +1 -0
  7. codecortex-0.24.0/src/codeintel/__init__.py +1 -0
  8. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/__main__.py +30 -2
  9. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/c4.py +2 -1
  10. codecortex-0.24.0/src/codeintel/commands/uninstall.py +106 -0
  11. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/doctor.py +5 -2
  12. codecortex-0.24.0/src/codeintel/filelock.py +120 -0
  13. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/gateway.py +164 -20
  14. codecortex-0.24.0/src/codeintel/graph_answer.py +970 -0
  15. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/graph_backend.py +5 -2
  16. codecortex-0.24.0/src/codeintel/graph_confidence.py +155 -0
  17. codecortex-0.24.0/src/codeintel/graph_edges.py +89 -0
  18. codecortex-0.24.0/src/codeintel/graph_ops.py +712 -0
  19. codecortex-0.24.0/src/codeintel/graph_render.py +295 -0
  20. codecortex-0.24.0/src/codeintel/graph_targets.py +138 -0
  21. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/grapher.py +2 -5
  22. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/installer.py +156 -0
  23. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/outcome.py +22 -0
  24. codecortex-0.24.0/src/codeintel/per_thread.py +64 -0
  25. codecortex-0.24.0/src/codeintel/provider.py +242 -0
  26. codecortex-0.24.0/src/codeintel/providers/graph.py +916 -0
  27. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/providers/lsp.py +460 -47
  28. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/providers/semantic.py +159 -3
  29. codecortex-0.24.0/src/codeintel/query_ops.py +32 -0
  30. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/reindexer.py +36 -11
  31. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/server.py +33 -11
  32. codecortex-0.24.0/tests/test_bench_corpus_typed.py +146 -0
  33. codecortex-0.24.0/tests/test_bench_gate.py +103 -0
  34. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_bench_oracle.py +24 -0
  35. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_bench_oracle_ts.py +125 -0
  36. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_corpus.py +2 -3
  37. codecortex-0.24.0/tests/test_docs_trust_claims.py +194 -0
  38. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_doctor.py +242 -1
  39. codecortex-0.24.0/tests/test_edge_confidence.py +687 -0
  40. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_enterprise.py +5 -0
  41. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_gateway.py +154 -1
  42. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_graph_failure_population.py +36 -13
  43. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_graph_provider.py +3 -3
  44. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_grapher.py +2 -1
  45. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_hardening.py +92 -3
  46. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_http_auth.py +8 -0
  47. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_http_server.py +1 -0
  48. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_incompleteness.py +1 -1
  49. codecortex-0.24.0/tests/test_index_incompleteness.py +237 -0
  50. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_installer.py +8 -1
  51. codecortex-0.24.0/tests/test_lsp_language_coverage.py +231 -0
  52. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_lsp_provider.py +515 -2
  53. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_mapper.py +15 -14
  54. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_mcp_handshake.py +6 -5
  55. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_mcp_server.py +36 -0
  56. codecortex-0.24.0/tests/test_missing_target.py +270 -0
  57. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_null_reason_and_ripple.py +201 -0
  58. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_onboarding.py +3 -5
  59. codecortex-0.24.0/tests/test_per_thread.py +101 -0
  60. codecortex-0.24.0/tests/test_query_budget.py +220 -0
  61. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_rbac.py +22 -6
  62. codecortex-0.24.0/tests/test_reindex_lock.py +209 -0
  63. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_reindexer.py +7 -0
  64. codecortex-0.24.0/tests/test_summary_integrity.py +2078 -0
  65. codecortex-0.24.0/tests/test_uninstall.py +335 -0
  66. codecortex-0.24.0/tests/test_upgrade_path.py +220 -0
  67. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_verify.py +3 -3
  68. codecortex-0.23.3/src/codeintel/__init__.py +0 -1
  69. codecortex-0.23.3/src/codeintel/graph_render.py +0 -95
  70. codecortex-0.23.3/src/codeintel/provider.py +0 -150
  71. codecortex-0.23.3/src/codeintel/providers/graph.py +0 -2445
  72. codecortex-0.23.3/src/codeintel/query_ops.py +0 -17
  73. codecortex-0.23.3/tests/test_edge_confidence.py +0 -151
  74. codecortex-0.23.3/tests/test_lsp_language_coverage.py +0 -100
  75. {codecortex-0.23.3 → codecortex-0.24.0}/LICENSE +0 -0
  76. {codecortex-0.23.3 → codecortex-0.24.0}/setup.cfg +0 -0
  77. {codecortex-0.23.3 → codecortex-0.24.0}/src/codecortex.egg-info/dependency_links.txt +0 -0
  78. {codecortex-0.23.3 → codecortex-0.24.0}/src/codecortex.egg-info/entry_points.txt +0 -0
  79. {codecortex-0.23.3 → codecortex-0.24.0}/src/codecortex.egg-info/top_level.txt +0 -0
  80. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/agent_prompt.py +0 -0
  81. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/auth.py +0 -0
  82. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/c4_check.py +0 -0
  83. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/c4_layers.py +0 -0
  84. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/cache.py +0 -0
  85. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/__init__.py +0 -0
  86. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/_common.py +0 -0
  87. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/c4.py +0 -0
  88. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/doctor.py +0 -0
  89. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/gen_token.py +0 -0
  90. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/graph.py +0 -0
  91. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/index.py +0 -0
  92. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/install.py +0 -0
  93. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/map.py +0 -0
  94. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/prompt.py +0 -0
  95. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/query.py +0 -0
  96. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/reset.py +0 -0
  97. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/serve.py +0 -0
  98. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/serve_http.py +0 -0
  99. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/setup.py +0 -0
  100. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/commands/status.py +0 -0
  101. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/config.py +0 -0
  102. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/containment.py +0 -0
  103. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/graph_resolution.py +0 -0
  104. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/http_server.py +0 -0
  105. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/indexer.py +0 -0
  106. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/injector.py +0 -0
  107. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/lang_config.py +0 -0
  108. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/loc.py +0 -0
  109. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/logconfig.py +0 -0
  110. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/mapper.py +0 -0
  111. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/metrics.py +0 -0
  112. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/onboarding.py +0 -0
  113. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/paths.py +0 -0
  114. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/policy.py +0 -0
  115. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/progress.py +0 -0
  116. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/providers/__init__.py +0 -0
  117. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/providers/none.py +0 -0
  118. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/py.typed +0 -0
  119. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/redact.py +0 -0
  120. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/reset.py +0 -0
  121. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/searcher.py +0 -0
  122. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/semantic_db.py +0 -0
  123. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/source_kind.py +0 -0
  124. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/term.py +0 -0
  125. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/verify.py +0 -0
  126. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/viewer/__init__.py +0 -0
  127. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/viewer/graph_template.html +0 -0
  128. {codecortex-0.23.3 → codecortex-0.24.0}/src/codeintel/wire_text.py +0 -0
  129. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_agent_prompt.py +0 -0
  130. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_backend_reaper.py +0 -0
  131. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_background_index_failure.py +0 -0
  132. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_c4.py +0 -0
  133. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_c4_check.py +0 -0
  134. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_c4_layers.py +0 -0
  135. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_c4_model.py +0 -0
  136. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_c4_output.py +0 -0
  137. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_cache.py +0 -0
  138. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_chunking.py +0 -0
  139. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_cli_commands.py +0 -0
  140. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_cli_help.py +0 -0
  141. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_cli_install_dry_run.py +0 -0
  142. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_cli_install_offer_injection.py +0 -0
  143. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_cold_process.py +0 -0
  144. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_config.py +0 -0
  145. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_could_not_ask_semantic.py +0 -0
  146. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_docs_ci_claims.py +0 -0
  147. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_docs_deadcode_withdrawal.py +0 -0
  148. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_e2e.py +0 -0
  149. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_engine_adoption.py +0 -0
  150. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_graph_real.py +0 -0
  151. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_graph_stdin.py +0 -0
  152. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_hard_exit.py +0 -0
  153. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_index_failure_reason.py +0 -0
  154. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_index_progress.py +0 -0
  155. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_indexer_binary.py +0 -0
  156. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_integration.py +0 -0
  157. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_lang_config.py +0 -0
  158. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_loc_census.py +0 -0
  159. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_lsp_real.py +0 -0
  160. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_model_dimension.py +0 -0
  161. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_never_raise.py +0 -0
  162. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_redaction_boundary.py +0 -0
  163. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_rerank.py +0 -0
  164. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_reset.py +0 -0
  165. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_semantic_provider.py +0 -0
  166. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_source_kind.py +0 -0
  167. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_staleness.py +0 -0
  168. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_symbol_context.py +0 -0
  169. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_term.py +0 -0
  170. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_treesitter.py +0 -0
  171. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_verify_call.py +0 -0
  172. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_version_skew.py +0 -0
  173. {codecortex-0.23.3 → codecortex-0.24.0}/tests/test_wire_text.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.23.3
3
+ Version: 0.24.0
4
4
  Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
5
5
  Author: Shammai Hamilton
6
6
  License-Expression: MIT
@@ -31,6 +31,7 @@ Requires-Dist: tree-sitter-language-pack>=1.0
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: pytest>=8; extra == "dev"
33
33
  Requires-Dist: pytest-cov>=5; extra == "dev"
34
+ Requires-Dist: pytest-timeout>=2.3; extra == "dev"
34
35
  Requires-Dist: numpy>=1.24; extra == "dev"
35
36
  Requires-Dist: ruff>=0.16; extra == "dev"
36
37
  Requires-Dist: mypy>=1.11; extra == "dev"
@@ -53,6 +54,10 @@ Dynamic: license-file
53
54
  > been pointed at an unfamiliar codebase it has found real bugs. **Use it locally, on a developer
54
55
  > machine, for a single user** — that is the case it is built and tested for. Before relying on it
55
56
  > for anything beyond that, read **[Project status](#project-status)**.
57
+ >
58
+ > **New here? Read [docs/trust.md](docs/trust.md) first** — what each engine's answer is worth, a
59
+ > five-minute check on a symbol whose callers you already know, and the eight states your
60
+ > repository can be in with one command for each.
56
61
 
57
62
  ![codeintel's own call graph — an interactive, self-contained HTML view with force / radial / layered / module layouts, complexity-sized nodes, and click-to-inspect metrics.](docs/images/graph-codeintel.png)
58
63
 
@@ -432,7 +437,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
432
437
  - **[Install & registration](docs/install.md)** — what each agent host actually reads, why the registered command is an absolute path, and the three levels of proof that registration worked.
433
438
  - **[Query flow](docs/query-flow.md)** — request lifecycle, engine selection, fan-out & merge, and why it never throws.
434
439
  - **[Map file](docs/map-file.md)** — the static `CODE_INTEL.md` orientation layer for hosts with no MCP support.
435
- - **[Benchmarks](docs/benchmarks.md)** — real numbers at scale: 25 k chunks indexed in ~8 min, ~235 ms warm queries, 60 MB index.
440
+ - **[Benchmarks](docs/benchmarks.md)** — real numbers at scale: 30 k chunks indexed in ~10 min, ~233 ms warm queries, 68 MB index.
436
441
  - Engine references: **[graph](docs/graph.md)** · **[lsp](docs/lsp.md)** · **[semantic](docs/semantic.md)**.
437
442
 
438
443
  ## CLI reference
@@ -448,7 +453,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
448
453
  | `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote] [--token TOKEN]` | Start the HTTP transport (loopback-only unless `--allow-remote`; `--token` requires a bearer token on every request) |
449
454
  | `codeintel query --op OP --target TARGET [--engine auto] [--project-root DIR] [--json]` | Run a single query and print the result |
450
455
  | `codeintel status [project_root]` | Show engine availability and index age |
451
- | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
456
+ | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap. `--deep` puts one **real query** to each engine and requires an answer with content — a process that starts is not a process that answers, and `runnable` means the second |
452
457
  | `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
453
458
  | `codeintel graph [project_root] [--html] [--out FILE] [--limit N]` | Emit the call graph as `{nodes,edges}` JSON, or `--html` a self-contained interactive viewer — see [docs/graph-viewer.md](docs/graph-viewer.md) |
454
459
  | `codeintel c4 [project_root] [--out DIR] [--depth N] [--scope PATH] [--include-tests] [--no-index] [--edges union\|imports] [--json] [--layers] [--suggest-config] [--check] [--layers-from auto\|inferred\|declared]` | Write a LikeC4 architecture model (`.c4`) of the repo's files and import graph. Indexes the repo first if it has no graph index. `--edges imports` drops the lower-confidence edges for a legible diagram; `--layers` reports inferred architectural bands; `--check` gates CI on a declared `[layers]` config, exit 2 on drift. See [An architecture model, as source](#an-architecture-model-as-source) and [docs/c4.md](docs/c4.md) |
@@ -13,6 +13,10 @@
13
13
  > been pointed at an unfamiliar codebase it has found real bugs. **Use it locally, on a developer
14
14
  > machine, for a single user** — that is the case it is built and tested for. Before relying on it
15
15
  > for anything beyond that, read **[Project status](#project-status)**.
16
+ >
17
+ > **New here? Read [docs/trust.md](docs/trust.md) first** — what each engine's answer is worth, a
18
+ > five-minute check on a symbol whose callers you already know, and the eight states your
19
+ > repository can be in with one command for each.
16
20
 
17
21
  ![codeintel's own call graph — an interactive, self-contained HTML view with force / radial / layered / module layouts, complexity-sized nodes, and click-to-inspect metrics.](docs/images/graph-codeintel.png)
18
22
 
@@ -392,7 +396,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
392
396
  - **[Install & registration](docs/install.md)** — what each agent host actually reads, why the registered command is an absolute path, and the three levels of proof that registration worked.
393
397
  - **[Query flow](docs/query-flow.md)** — request lifecycle, engine selection, fan-out & merge, and why it never throws.
394
398
  - **[Map file](docs/map-file.md)** — the static `CODE_INTEL.md` orientation layer for hosts with no MCP support.
395
- - **[Benchmarks](docs/benchmarks.md)** — real numbers at scale: 25 k chunks indexed in ~8 min, ~235 ms warm queries, 60 MB index.
399
+ - **[Benchmarks](docs/benchmarks.md)** — real numbers at scale: 30 k chunks indexed in ~10 min, ~233 ms warm queries, 68 MB index.
396
400
  - Engine references: **[graph](docs/graph.md)** · **[lsp](docs/lsp.md)** · **[semantic](docs/semantic.md)**.
397
401
 
398
402
  ## CLI reference
@@ -408,7 +412,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
408
412
  | `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote] [--token TOKEN]` | Start the HTTP transport (loopback-only unless `--allow-remote`; `--token` requires a bearer token on every request) |
409
413
  | `codeintel query --op OP --target TARGET [--engine auto] [--project-root DIR] [--json]` | Run a single query and print the result |
410
414
  | `codeintel status [project_root]` | Show engine availability and index age |
411
- | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
415
+ | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap. `--deep` puts one **real query** to each engine and requires an answer with content — a process that starts is not a process that answers, and `runnable` means the second |
412
416
  | `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
413
417
  | `codeintel graph [project_root] [--html] [--out FILE] [--limit N]` | Emit the call graph as `{nodes,edges}` JSON, or `--html` a self-contained interactive viewer — see [docs/graph-viewer.md](docs/graph-viewer.md) |
414
418
  | `codeintel c4 [project_root] [--out DIR] [--depth N] [--scope PATH] [--include-tests] [--no-index] [--edges union\|imports] [--json] [--layers] [--suggest-config] [--check] [--layers-from auto\|inferred\|declared]` | Write a LikeC4 architecture model (`.c4`) of the repo's files and import graph. Indexes the repo first if it has no graph index. `--edges imports` drops the lower-confidence edges for a legible diagram; `--layers` reports inferred architectural bands; `--check` gates CI on a declared `[layers]` config, exit 2 on drift. See [An architecture model, as source](#an-architecture-model-as-source) and [docs/c4.md](docs/c4.md) |
@@ -53,7 +53,8 @@ Changelog = "https://github.com/hamilton-sky/codeintel/blob/main/CHANGELOG.md"
53
53
  codeintel = "codeintel.__main__:main"
54
54
 
55
55
  [project.optional-dependencies]
56
- dev = ["pytest>=8", "pytest-cov>=5", "numpy>=1.24", "ruff>=0.16", "mypy>=1.11"]
56
+ dev = ["pytest>=8", "pytest-cov>=5", "pytest-timeout>=2.3", "numpy>=1.24", "ruff>=0.16",
57
+ "mypy>=1.11"]
57
58
 
58
59
  # `bench/agent_bench.py` only. Deliberately NOT in `dev`, and emphatically not a runtime dependency:
59
60
  # codeintel's own code makes zero outbound HTTP calls and that claim is load-bearing in the README.
@@ -161,3 +162,34 @@ follow_imports = "skip"
161
162
  # case in CI, and that costs ~0.4 points. A floor pinned to today's measurement would go red on a
162
163
  # runner's toolchain rather than on a real coverage regression. Ratchet it up deliberately.
163
164
  addopts = "--cov=codeintel --cov-report=term-missing:skip-covered --cov-fail-under=83"
165
+
166
+ # No test may run indefinitely. The ceiling is deliberately far above any legitimate test rather
167
+ # than tuned to the current slowest: its job is to convert a hang into a named failure, not to
168
+ # police performance. Measured worst cases after the background-reindex guard landed —
169
+ # `test_cold_process` spawns real cold interpreters at ~60s, `test_hard_exit` runs nested pytest
170
+ # sessions at ~12s — so 180s leaves room for a slow CI runner and still fails in three minutes
171
+ # instead of never.
172
+ #
173
+ # `signal` rather than `thread`: SIGALRM fails the ONE test that overran and lets the session carry
174
+ # on to a normal report. pytest-timeout's `thread` method kills the whole process, which would
175
+ # reproduce the thing being fixed — an unexplained death partway through — with better provenance
176
+ # but no more information. A test that needs longer says so with
177
+ # `@pytest.mark.timeout(<seconds>)` and, ideally, why.
178
+ timeout = 180
179
+ timeout_method = "signal"
180
+
181
+ # A leaked socket, pipe or file handle is a defect, not a note. Promoted to an error once the
182
+ # suite's own leaks were closed — sixteen HTTP listeners kept open by `shutdown()` without
183
+ # `server_close()`, a template file, eight bare `open(...).read()` calls in `test_mapper`, and two
184
+ # MCP children killed without being waited on or having their pipes closed. Leaving it at
185
+ # `warning` is what let all of those accumulate: the count was in every run's output and had been
186
+ # for months.
187
+ #
188
+ # `default::ResourceWarning:_pytest.unraisableexception` is NOT an exemption for our own leaks —
189
+ # pytest surfaces a garbage-collected leak through that module rather than from the test that
190
+ # caused it, and as an error it fails whichever unrelated test happened to trigger the collection.
191
+ # Our leaks are caught at their own site by the first rule; this keeps the blame accurate.
192
+ filterwarnings = [
193
+ "error::ResourceWarning",
194
+ "default::ResourceWarning:_pytest.unraisableexception",
195
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.23.3
3
+ Version: 0.24.0
4
4
  Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
5
5
  Author: Shammai Hamilton
6
6
  License-Expression: MIT
@@ -31,6 +31,7 @@ Requires-Dist: tree-sitter-language-pack>=1.0
31
31
  Provides-Extra: dev
32
32
  Requires-Dist: pytest>=8; extra == "dev"
33
33
  Requires-Dist: pytest-cov>=5; extra == "dev"
34
+ Requires-Dist: pytest-timeout>=2.3; extra == "dev"
34
35
  Requires-Dist: numpy>=1.24; extra == "dev"
35
36
  Requires-Dist: ruff>=0.16; extra == "dev"
36
37
  Requires-Dist: mypy>=1.11; extra == "dev"
@@ -53,6 +54,10 @@ Dynamic: license-file
53
54
  > been pointed at an unfamiliar codebase it has found real bugs. **Use it locally, on a developer
54
55
  > machine, for a single user** — that is the case it is built and tested for. Before relying on it
55
56
  > for anything beyond that, read **[Project status](#project-status)**.
57
+ >
58
+ > **New here? Read [docs/trust.md](docs/trust.md) first** — what each engine's answer is worth, a
59
+ > five-minute check on a symbol whose callers you already know, and the eight states your
60
+ > repository can be in with one command for each.
56
61
 
57
62
  ![codeintel's own call graph — an interactive, self-contained HTML view with force / radial / layered / module layouts, complexity-sized nodes, and click-to-inspect metrics.](docs/images/graph-codeintel.png)
58
63
 
@@ -432,7 +437,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
432
437
  - **[Install & registration](docs/install.md)** — what each agent host actually reads, why the registered command is an absolute path, and the three levels of proof that registration worked.
433
438
  - **[Query flow](docs/query-flow.md)** — request lifecycle, engine selection, fan-out & merge, and why it never throws.
434
439
  - **[Map file](docs/map-file.md)** — the static `CODE_INTEL.md` orientation layer for hosts with no MCP support.
435
- - **[Benchmarks](docs/benchmarks.md)** — real numbers at scale: 25 k chunks indexed in ~8 min, ~235 ms warm queries, 60 MB index.
440
+ - **[Benchmarks](docs/benchmarks.md)** — real numbers at scale: 30 k chunks indexed in ~10 min, ~233 ms warm queries, 68 MB index.
436
441
  - Engine references: **[graph](docs/graph.md)** · **[lsp](docs/lsp.md)** · **[semantic](docs/semantic.md)**.
437
442
 
438
443
  ## CLI reference
@@ -448,7 +453,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
448
453
  | `codeintel serve-http [--host HOST] [--port 8766] [--allow-remote] [--token TOKEN]` | Start the HTTP transport (loopback-only unless `--allow-remote`; `--token` requires a bearer token on every request) |
449
454
  | `codeintel query --op OP --target TARGET [--engine auto] [--project-root DIR] [--json]` | Run a single query and print the result |
450
455
  | `codeintel status [project_root]` | Show engine availability and index age |
451
- | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
456
+ | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap. `--deep` puts one **real query** to each engine and requires an answer with content — a process that starts is not a process that answers, and `runnable` means the second |
452
457
  | `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
453
458
  | `codeintel graph [project_root] [--html] [--out FILE] [--limit N]` | Emit the call graph as `{nodes,edges}` JSON, or `--html` a self-contained interactive viewer — see [docs/graph-viewer.md](docs/graph-viewer.md) |
454
459
  | `codeintel c4 [project_root] [--out DIR] [--depth N] [--scope PATH] [--include-tests] [--no-index] [--edges union\|imports] [--json] [--layers] [--suggest-config] [--check] [--layers-from auto\|inferred\|declared]` | Write a LikeC4 architecture model (`.c4`) of the repo's files and import graph. Indexes the repo first if it has no graph index. `--edges imports` drops the lower-confidence edges for a legible diagram; `--layers` reports inferred architectural bands; `--check` gates CI on a declared `[layers]` config, exit 2 on drift. See [An architecture model, as source](#an-architecture-model-as-source) and [docs/c4.md](docs/c4.md) |
@@ -18,10 +18,16 @@ src/codeintel/cache.py
18
18
  src/codeintel/config.py
19
19
  src/codeintel/containment.py
20
20
  src/codeintel/doctor.py
21
+ src/codeintel/filelock.py
21
22
  src/codeintel/gateway.py
23
+ src/codeintel/graph_answer.py
22
24
  src/codeintel/graph_backend.py
25
+ src/codeintel/graph_confidence.py
26
+ src/codeintel/graph_edges.py
27
+ src/codeintel/graph_ops.py
23
28
  src/codeintel/graph_render.py
24
29
  src/codeintel/graph_resolution.py
30
+ src/codeintel/graph_targets.py
25
31
  src/codeintel/grapher.py
26
32
  src/codeintel/http_server.py
27
33
  src/codeintel/indexer.py
@@ -35,6 +41,7 @@ src/codeintel/metrics.py
35
41
  src/codeintel/onboarding.py
36
42
  src/codeintel/outcome.py
37
43
  src/codeintel/paths.py
44
+ src/codeintel/per_thread.py
38
45
  src/codeintel/policy.py
39
46
  src/codeintel/progress.py
40
47
  src/codeintel/provider.py
@@ -66,6 +73,7 @@ src/codeintel/commands/serve.py
66
73
  src/codeintel/commands/serve_http.py
67
74
  src/codeintel/commands/setup.py
68
75
  src/codeintel/commands/status.py
76
+ src/codeintel/commands/uninstall.py
69
77
  src/codeintel/providers/__init__.py
70
78
  src/codeintel/providers/graph.py
71
79
  src/codeintel/providers/lsp.py
@@ -76,6 +84,8 @@ src/codeintel/viewer/graph_template.html
76
84
  tests/test_agent_prompt.py
77
85
  tests/test_backend_reaper.py
78
86
  tests/test_background_index_failure.py
87
+ tests/test_bench_corpus_typed.py
88
+ tests/test_bench_gate.py
79
89
  tests/test_bench_oracle.py
80
90
  tests/test_bench_oracle_ts.py
81
91
  tests/test_c4.py
@@ -95,6 +105,7 @@ tests/test_corpus.py
95
105
  tests/test_could_not_ask_semantic.py
96
106
  tests/test_docs_ci_claims.py
97
107
  tests/test_docs_deadcode_withdrawal.py
108
+ tests/test_docs_trust_claims.py
98
109
  tests/test_doctor.py
99
110
  tests/test_e2e.py
100
111
  tests/test_edge_confidence.py
@@ -112,6 +123,7 @@ tests/test_http_auth.py
112
123
  tests/test_http_server.py
113
124
  tests/test_incompleteness.py
114
125
  tests/test_index_failure_reason.py
126
+ tests/test_index_incompleteness.py
115
127
  tests/test_index_progress.py
116
128
  tests/test_indexer_binary.py
117
129
  tests/test_installer.py
@@ -124,21 +136,28 @@ tests/test_lsp_real.py
124
136
  tests/test_mapper.py
125
137
  tests/test_mcp_handshake.py
126
138
  tests/test_mcp_server.py
139
+ tests/test_missing_target.py
127
140
  tests/test_model_dimension.py
128
141
  tests/test_never_raise.py
129
142
  tests/test_null_reason_and_ripple.py
130
143
  tests/test_onboarding.py
144
+ tests/test_per_thread.py
145
+ tests/test_query_budget.py
131
146
  tests/test_rbac.py
132
147
  tests/test_redaction_boundary.py
148
+ tests/test_reindex_lock.py
133
149
  tests/test_reindexer.py
134
150
  tests/test_rerank.py
135
151
  tests/test_reset.py
136
152
  tests/test_semantic_provider.py
137
153
  tests/test_source_kind.py
138
154
  tests/test_staleness.py
155
+ tests/test_summary_integrity.py
139
156
  tests/test_symbol_context.py
140
157
  tests/test_term.py
141
158
  tests/test_treesitter.py
159
+ tests/test_uninstall.py
160
+ tests/test_upgrade_path.py
142
161
  tests/test_verify.py
143
162
  tests/test_verify_call.py
144
163
  tests/test_version_skew.py
@@ -9,6 +9,7 @@ anthropic>=0.40
9
9
  [dev]
10
10
  pytest>=8
11
11
  pytest-cov>=5
12
+ pytest-timeout>=2.3
12
13
  numpy>=1.24
13
14
  ruff>=0.16
14
15
  mypy>=1.11
@@ -0,0 +1 @@
1
+ __version__ = "0.24.0"
@@ -44,6 +44,7 @@ _COMMAND_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
44
44
  ("setup", "Prepare backends + index this repo (--all does it all)"),
45
45
  ("index", "Build the index every other command reads (semantic + graph)"),
46
46
  ("install", "Register codeintel with the AI agents on this machine"),
47
+ ("uninstall", "Remove those registrations — the index is left alone"),
47
48
  ("prompt", "Print a paste-to-your-agent setup prompt for this machine"),
48
49
  ]),
49
50
  ("Check health", [
@@ -79,6 +80,7 @@ _MODULES = {
79
80
  "setup": "setup",
80
81
  "index": "index",
81
82
  "install": "install",
83
+ "uninstall": "uninstall",
82
84
  "prompt": "prompt",
83
85
  "doctor": "doctor",
84
86
  "status": "status",
@@ -333,6 +335,13 @@ The one-shot path on a new machine.""",
333
335
  Writes the MCP server config so an agent can call codeintel. Then restart the
334
336
  agent (or start a new session) — a running host does not reload its MCP
335
337
  config. `codeintel doctor` lists what got registered where.""",
338
+ "uninstall": """examples:
339
+ codeintel uninstall
340
+ codeintel uninstall --dry-run show what would go, change nothing
341
+
342
+ Removes only codeintel's own entry from each agent config — neighbouring
343
+ servers and your settings are left alone. The index is NOT deleted; use
344
+ `codeintel reset --all` for that.""",
336
345
  "prompt": """examples:
337
346
  codeintel prompt
338
347
 
@@ -462,7 +471,8 @@ def build_parser() -> argparse.ArgumentParser:
462
471
  "--json", action="store_true", help="Emit the structured status and index-age report")
463
472
  status_parser.add_argument(
464
473
  "--deep", action="store_true",
465
- help="Boot LSP and sample indexed source readability (slower, read-only)")
474
+ help="Put a real query to each engine and require an answer, boot the LSP, and sample "
475
+ "indexed source readability (slower, read-only)")
466
476
 
467
477
  # serve-http subcommand
468
478
  http_parser = subparsers.add_parser(
@@ -503,6 +513,22 @@ def build_parser() -> argparse.ArgumentParser:
503
513
  help="Show what would be registered, and where — write nothing",
504
514
  )
505
515
 
516
+ # uninstall subcommand
517
+ uninstall_parser = subparsers.add_parser(
518
+ "uninstall", help=_DESCRIPTIONS["uninstall"], description=_DESCRIPTIONS["uninstall"])
519
+ uninstall_parser.add_argument(
520
+ "--agent",
521
+ choices=["auto", "claude", "codex", "gemini", "zed", "all"],
522
+ default="auto",
523
+ help="Agent to remove from (default: auto — every agent codeintel is REGISTERED with, "
524
+ "which is a different set from the agents installed here)",
525
+ )
526
+ uninstall_parser.add_argument(
527
+ "--dry-run",
528
+ action="store_true",
529
+ help="Show what would be removed, and from where — change nothing",
530
+ )
531
+
506
532
  # map subcommand
507
533
  map_parser = subparsers.add_parser(
508
534
  "map", help=_DESCRIPTIONS["map"], description=_DESCRIPTIONS["map"])
@@ -575,7 +601,9 @@ def build_parser() -> argparse.ArgumentParser:
575
601
  "doctor", help=_DESCRIPTIONS["doctor"], description=_DESCRIPTIONS["doctor"])
576
602
  doctor_parser.add_argument("project_root", nargs="?", default=None, help="Project root (default: cwd)")
577
603
  doctor_parser.add_argument("--deep", action="store_true",
578
- help="Also boot-check serena (slower; first boot pulls it via uvx)")
604
+ help="Ask each engine one REAL query and require content — a "
605
+ "process that starts is not one that answers. Also boots "
606
+ "serena (slower; first boot pulls it via uvx)")
579
607
  doctor_parser.add_argument("--json", action="store_true",
580
608
  help="Emit the structured JSON report instead of the table")
581
609
 
@@ -495,7 +495,8 @@ def build_c4_payload(project_root: Any, *, depth: int | None = None, scope: tupl
495
495
  payload at an auto-fit (or requested) directory depth. Never raises -- returns a flat `reason`
496
496
  string and empty lists on any failure."""
497
497
  try:
498
- from codeintel.providers.graph import GraphProvider, _repo_display_name
498
+ from codeintel.graph_render import _repo_display_name
499
+ from codeintel.providers.graph import GraphProvider
499
500
 
500
501
  p = GraphProvider()
501
502
  if not getattr(p, "available", False):
@@ -0,0 +1,106 @@
1
+ """`codeintel uninstall` — remove codeintel's entry from the agent configs `install` wrote.
2
+
3
+ The inverse of `install`, and deliberately only that. It does NOT delete the semantic index: that
4
+ is `reset`'s job, it is expensive to rebuild (ten minutes on a large repo), and a command whose
5
+ name says "uninstall" quietly destroying ten minutes of work is the kind of surprise this project
6
+ spends most of its comments avoiding. What the index costs and how to remove it is printed instead,
7
+ which is the same rule every other disclosure here follows — name what was NOT done.
8
+
9
+ There is no confirmation prompt, and that is a considered difference from `reset --all`. This edits
10
+ one entry in a config file and `codeintel install` puts it straight back; `reset` destroys an index
11
+ that has to be re-embedded. `--dry-run` covers the reader who wants to look first.
12
+ """
13
+
14
+ from typing import Any
15
+
16
+ from codeintel.commands._common import never_raise
17
+
18
+
19
+ def _targets(agent: str, installer_mod: Any) -> tuple[list[str], list[str]]:
20
+ """`(agents, untouched)` for the requested `--agent` selection.
21
+
22
+ `auto` means "wherever codeintel is actually registered", not "agents installed on this
23
+ machine" — the question `install` asks. They differ in both directions that matter: an agent
24
+ uninstalled from this machine can still hold our entry, and an agent that is present but never
25
+ registered has nothing to remove and should not be reported as though it did.
26
+ """
27
+ if agent == "auto":
28
+ registered = installer_mod.registered_agents()
29
+ return registered, [a for a in installer_mod._AGENTS if a not in registered]
30
+ if agent == "all":
31
+ return list(installer_mod._AGENTS), []
32
+ return [agent], []
33
+
34
+
35
+ def _dry_run_line(installer_mod: Any, agent: str) -> str:
36
+ """What `--dry-run` would do to one agent's config — read-only, through the same lookup
37
+ `codeintel doctor` uses to notice a registration at all."""
38
+ spec = installer_mod._CONFIG[agent]
39
+ path, current = installer_mod.registered_command(spec)
40
+ if current is None:
41
+ return f"~ {agent}: no codeintel entry in {path} (no change)"
42
+ return f"- {agent}: would remove the codeintel entry from {path} (command: {current})"
43
+
44
+
45
+ def _report(results: list) -> tuple[bool, bool, list[str]]:
46
+ """Print one line per agent; return (removed_any, failed_any, legacy_paths)."""
47
+ removed_any = failed_any = False
48
+ legacy_paths: list[str] = []
49
+ for r in results:
50
+ agent, path, action = r["agent"], r["path"], r["action"]
51
+ if action == "removed":
52
+ print(f"v {agent}: removed from {path}")
53
+ removed_any = True
54
+ elif action == "absent":
55
+ print(f"~ {agent}: nothing registered at {path}")
56
+ else:
57
+ print(f"x {agent}: failed — {r['reason']}")
58
+ failed_any = True
59
+ if r.get("legacy"):
60
+ legacy_paths.append(r["legacy"])
61
+ return removed_any, failed_any, legacy_paths
62
+
63
+
64
+ @never_raise("uninstall failed: {exc}", code=1)
65
+ def run(args: Any) -> int:
66
+ from codeintel import installer as installer_mod
67
+
68
+ agents, untouched = _targets(args.agent, installer_mod)
69
+
70
+ if args.agent == "auto" and not agents:
71
+ # Exit 0, not 1. The requested end state — codeintel is not registered with anything —
72
+ # already holds, and reporting "failure" for a goal that is met would make this command
73
+ # unusable in a teardown script that runs whether or not install ever ran.
74
+ print("codeintel is not registered with any agent on this machine — nothing to remove.")
75
+ print(f" (looked at: {', '.join(untouched)})")
76
+ return 0
77
+
78
+ if getattr(args, "dry_run", False):
79
+ for agent in agents:
80
+ print(_dry_run_line(installer_mod, agent))
81
+ print("\nDry run — nothing was changed.")
82
+ return 0
83
+
84
+ results = installer_mod.Installer().unregister_many(agents)
85
+ removed_any, failed_any, legacy_paths = _report(results)
86
+
87
+ for legacy in dict.fromkeys(legacy_paths):
88
+ print(f"\n! stale entry: {legacy} has an `mcpServers.codeintel` block that this host does "
89
+ f"NOT read (an older codeintel wrote it). It is inert, and safe to delete by hand.")
90
+
91
+ if args.agent == "auto" and untouched:
92
+ print(f"\n- untouched (nothing registered): {', '.join(untouched)}")
93
+
94
+ if removed_any:
95
+ # A host that is already running keeps the server it launched at session start, so the
96
+ # tools stay on screen until it is restarted — the mirror of the note `install` prints,
97
+ # and the same surprise in the other direction.
98
+ print("\nRestart your agent (or start a new session) — a running host does not reload "
99
+ "its MCP config, so codeintel's tools stay visible until it does.")
100
+
101
+ # Named, not done. The index is the expensive artifact and this command does not own it.
102
+ from codeintel.paths import codeintel_home
103
+ print(f"\nThe index is untouched: {codeintel_home()}")
104
+ print(" Remove it with `codeintel reset --all` if you are done with codeintel entirely.")
105
+
106
+ return 1 if failed_any else 0
@@ -280,7 +280,7 @@ def run_doctor(
280
280
  try:
281
281
  from codeintel.providers.graph import GraphProvider
282
282
  engines["graph"] = _probe_engine(
283
- "graph", graph, GraphProvider, lambda p: p.probe(root), on_provider
283
+ "graph", graph, GraphProvider, lambda p: p.probe(root, deep=deep), on_provider
284
284
  )
285
285
  except Exception:
286
286
  engines["graph"] = {"engine": "graph", "status": "fail", "installed": False,
@@ -414,7 +414,10 @@ def render_doctor_text(report: dict) -> str:
414
414
  ready, total, healthy = summ.get("ready", "?"), summ.get("total", "?"), summ.get("healthy")
415
415
  count = c.bold(f"{ready} / {total}")
416
416
  count = c.red(count) if healthy is False else (c.green(count) if healthy else count)
417
- tail = "" if report.get("deep") else c.dim(" (run with --deep to boot-check serena)")
417
+ # Names what --deep actually does. "boot-check serena" described one third of it and the
418
+ # least useful third: booting is the question this tool keeps learning not to stop at.
419
+ tail = "" if report.get("deep") else c.dim(
420
+ " (run with --deep to ask each engine a real query)")
418
421
  out.append("")
419
422
  out.append(f" {count} engines ready for this repo.{tail}")
420
423
  opt_down = [n for n in _ENGINES
@@ -0,0 +1,120 @@
1
+ """A cross-process advisory lock, used to stop two codeintel processes reindexing one repo at once.
2
+
3
+ MEASURED 2026-09-18: two `codeintel index` passes started together on a 600-file repository each
4
+ embedded all 600 chunks and each reported "Indexed 600 chunks". There is no natural serialisation —
5
+ both read the same "what is new" answer from the database before either had written anything, so
6
+ the whole embedding cost is paid twice. On a large repository that is ten minutes of CPU doubled,
7
+ and the two passes contend for the same cores while doing it.
8
+
9
+ WHY `flock` AND NOT A FLAG FILE. The obvious way to record "a reindex is running" is to write a
10
+ file and delete it at the end. That is strictly WORSE than keeping the state in memory: a process
11
+ that is killed never runs its cleanup, so the flag survives forever and the tool reports a reindex
12
+ in progress that is not. A signal true of a FILE and false of the WORLD is the defect this
13
+ repository catalogues in `tests/test_summary_integrity.py`, and buying one to save some CPU would
14
+ be a poor trade.
15
+
16
+ `flock` has no such failure mode, and that is the entire reason it is the primitive here: the lock
17
+ belongs to an open file descriptor, and **the kernel releases it when the process dies**, however it
18
+ dies. There is no cleanup to skip and no staleness to age out. The lock file itself is left behind,
19
+ which is fine — it is an empty file whose existence means nothing; only the lock on it does.
20
+
21
+ DEGRADES, NEVER BLOCKS THE WORK. `fcntl` is POSIX and this package claims `OS Independent`, so
22
+ every entry point here has a fallback, and the two fallbacks are deliberately asymmetric:
23
+
24
+ * failing to ACQUIRE degrades to "you hold it" — indexing still happens, exactly as it did before
25
+ this module existed. A locking bug must never be able to stop a repository being indexed.
26
+ * failing to PROBE degrades to "nobody holds it" — we do not claim a reindex is running on evidence
27
+ we could not gather. That is the same rule the envelope's `gaps` follow one level up.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import contextlib
32
+ import logging
33
+ import os
34
+ import pathlib
35
+ import re
36
+ from collections.abc import Iterator
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ try: # POSIX only; Windows has no fcntl
41
+ import fcntl
42
+ _HAVE_FLOCK = True
43
+ except Exception: # pragma: no cover - platform dependent
44
+ fcntl = None # type: ignore[assignment]
45
+ _HAVE_FLOCK = False
46
+
47
+
48
+ def lock_path(canonical_root: str) -> pathlib.Path:
49
+ """The lock file for one repository root.
50
+
51
+ Slugged the same way the graph cache names its files — every run of non-alphanumeric characters
52
+ collapses to a single `-` — which also flattens away every path separator, so a crafted root can
53
+ never place the lock outside the lock directory.
54
+ """
55
+ from codeintel.paths import codeintel_home
56
+
57
+ slug = re.sub(r"[^A-Za-z0-9]+", "-", canonical_root).strip("-") or "root"
58
+ return codeintel_home() / "locks" / f"reindex-{slug}.lock"
59
+
60
+
61
+ @contextlib.contextmanager
62
+ def exclusive(path: pathlib.Path) -> Iterator[bool]:
63
+ """Hold an exclusive, non-blocking lock on *path*. Yields whether we got it.
64
+
65
+ `False` means another LIVE process holds it — not that one once did. Callers use that to skip
66
+ work somebody else is already doing, so the answer has to be about the world rather than about
67
+ a file that happens to exist.
68
+ """
69
+ if not _HAVE_FLOCK:
70
+ # No advisory locking available: behave exactly as the code did before this module.
71
+ yield True
72
+ return
73
+
74
+ fd = None
75
+ try:
76
+ path.parent.mkdir(parents=True, exist_ok=True)
77
+ fd = os.open(str(path), os.O_CREAT | os.O_RDWR, 0o644)
78
+ except Exception as exc:
79
+ # An unwritable home is not a reason to stop indexing.
80
+ logger.debug("reindex lock unavailable at %s: %s", path, exc)
81
+ if fd is not None:
82
+ with contextlib.suppress(Exception):
83
+ os.close(fd)
84
+ yield True
85
+ return
86
+
87
+ acquired = False
88
+ try:
89
+ try:
90
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
91
+ acquired = True
92
+ except OSError:
93
+ acquired = False # somebody else is mid-pass
94
+ yield acquired
95
+ finally:
96
+ with contextlib.suppress(Exception):
97
+ if acquired:
98
+ fcntl.flock(fd, fcntl.LOCK_UN)
99
+ os.close(fd) # closing would release it anyway; explicit is clearer
100
+
101
+
102
+ def held_by_another_process(path: pathlib.Path) -> bool:
103
+ """True when some other live process holds this lock.
104
+
105
+ Implemented by trying to take it and letting go immediately, because that is the only question
106
+ the kernel will answer — "is this lock free right now". The momentary acquisition is harmless:
107
+ a real pass that loses this race retries on the next debounce window, and the alternative
108
+ (recording holders in a file) is the stale-state design this module exists to avoid.
109
+
110
+ Degrades to False. Reporting "a reindex is running" on evidence we could not gather would be a
111
+ claim about the world made from a failed syscall.
112
+ """
113
+ if not _HAVE_FLOCK or not path.exists():
114
+ return False
115
+ try:
116
+ with exclusive(path) as got:
117
+ return not got
118
+ except Exception as exc:
119
+ logger.debug("reindex lock probe failed for %s: %s", path, exc)
120
+ return False