codecortex 0.17.0__tar.gz → 0.18.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 (124) hide show
  1. {codecortex-0.17.0/src/codecortex.egg-info → codecortex-0.18.0}/PKG-INFO +5 -5
  2. {codecortex-0.17.0 → codecortex-0.18.0}/README.md +4 -4
  3. {codecortex-0.17.0 → codecortex-0.18.0/src/codecortex.egg-info}/PKG-INFO +5 -5
  4. {codecortex-0.17.0 → codecortex-0.18.0}/src/codecortex.egg-info/SOURCES.txt +4 -0
  5. codecortex-0.18.0/src/codeintel/__init__.py +1 -0
  6. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/auth.py +8 -1
  7. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/config.py +24 -2
  8. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/containment.py +15 -0
  9. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/indexer.py +160 -31
  10. codecortex-0.18.0/src/codeintel/paths.py +31 -0
  11. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/providers/semantic.py +108 -7
  12. codecortex-0.18.0/src/codeintel/redact.py +187 -0
  13. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/searcher.py +125 -32
  14. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/semantic_db.py +59 -16
  15. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_chunking.py +6 -6
  16. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_config.py +63 -3
  17. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_rbac.py +73 -0
  18. codecortex-0.18.0/tests/test_redaction_boundary.py +217 -0
  19. codecortex-0.18.0/tests/test_staleness.py +265 -0
  20. codecortex-0.18.0/tests/test_symbol_context.py +172 -0
  21. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_treesitter.py +5 -5
  22. codecortex-0.17.0/src/codeintel/__init__.py +0 -1
  23. codecortex-0.17.0/src/codeintel/redact.py +0 -113
  24. {codecortex-0.17.0 → codecortex-0.18.0}/LICENSE +0 -0
  25. {codecortex-0.17.0 → codecortex-0.18.0}/pyproject.toml +0 -0
  26. {codecortex-0.17.0 → codecortex-0.18.0}/setup.cfg +0 -0
  27. {codecortex-0.17.0 → codecortex-0.18.0}/src/codecortex.egg-info/dependency_links.txt +0 -0
  28. {codecortex-0.17.0 → codecortex-0.18.0}/src/codecortex.egg-info/entry_points.txt +0 -0
  29. {codecortex-0.17.0 → codecortex-0.18.0}/src/codecortex.egg-info/requires.txt +0 -0
  30. {codecortex-0.17.0 → codecortex-0.18.0}/src/codecortex.egg-info/top_level.txt +0 -0
  31. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/__main__.py +0 -0
  32. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/agent_prompt.py +0 -0
  33. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/cache.py +0 -0
  34. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/__init__.py +0 -0
  35. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/_common.py +0 -0
  36. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/doctor.py +0 -0
  37. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/gen_token.py +0 -0
  38. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/graph.py +0 -0
  39. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/index.py +0 -0
  40. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/install.py +0 -0
  41. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/map.py +0 -0
  42. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/prompt.py +0 -0
  43. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/query.py +0 -0
  44. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/reset.py +0 -0
  45. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/serve.py +0 -0
  46. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/serve_http.py +0 -0
  47. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/setup.py +0 -0
  48. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/commands/status.py +0 -0
  49. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/doctor.py +0 -0
  50. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/gateway.py +0 -0
  51. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/graph_backend.py +0 -0
  52. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/graph_render.py +0 -0
  53. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/graph_resolution.py +0 -0
  54. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/grapher.py +0 -0
  55. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/http_server.py +0 -0
  56. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/injector.py +0 -0
  57. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/installer.py +0 -0
  58. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/loc.py +0 -0
  59. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/logconfig.py +0 -0
  60. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/mapper.py +0 -0
  61. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/metrics.py +0 -0
  62. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/onboarding.py +0 -0
  63. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/outcome.py +0 -0
  64. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/policy.py +0 -0
  65. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/progress.py +0 -0
  66. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/provider.py +0 -0
  67. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/providers/__init__.py +0 -0
  68. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/providers/graph.py +0 -0
  69. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/providers/lsp.py +0 -0
  70. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/providers/none.py +0 -0
  71. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/py.typed +0 -0
  72. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/reindexer.py +0 -0
  73. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/reset.py +0 -0
  74. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/server.py +0 -0
  75. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/source_kind.py +0 -0
  76. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/term.py +0 -0
  77. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/verify.py +0 -0
  78. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/viewer/__init__.py +0 -0
  79. {codecortex-0.17.0 → codecortex-0.18.0}/src/codeintel/viewer/graph_template.html +0 -0
  80. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_agent_prompt.py +0 -0
  81. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_backend_reaper.py +0 -0
  82. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_cache.py +0 -0
  83. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_cli_commands.py +0 -0
  84. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_cli_help.py +0 -0
  85. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_cold_process.py +0 -0
  86. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_corpus.py +0 -0
  87. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_docs_ci_claims.py +0 -0
  88. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_docs_deadcode_withdrawal.py +0 -0
  89. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_doctor.py +0 -0
  90. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_e2e.py +0 -0
  91. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_engine_adoption.py +0 -0
  92. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_enterprise.py +0 -0
  93. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_gateway.py +0 -0
  94. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_graph_failure_population.py +0 -0
  95. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_graph_provider.py +0 -0
  96. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_graph_real.py +0 -0
  97. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_graph_stdin.py +0 -0
  98. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_grapher.py +0 -0
  99. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_hardening.py +0 -0
  100. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_http_auth.py +0 -0
  101. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_http_server.py +0 -0
  102. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_incompleteness.py +0 -0
  103. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_index_progress.py +0 -0
  104. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_indexer_binary.py +0 -0
  105. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_installer.py +0 -0
  106. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_integration.py +0 -0
  107. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_loc_census.py +0 -0
  108. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_lsp_provider.py +0 -0
  109. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_lsp_real.py +0 -0
  110. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_mapper.py +0 -0
  111. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_mcp_handshake.py +0 -0
  112. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_mcp_server.py +0 -0
  113. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_model_dimension.py +0 -0
  114. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_never_raise.py +0 -0
  115. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_onboarding.py +0 -0
  116. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_reindexer.py +0 -0
  117. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_rerank.py +0 -0
  118. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_reset.py +0 -0
  119. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_semantic_provider.py +0 -0
  120. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_source_kind.py +0 -0
  121. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_term.py +0 -0
  122. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_verify.py +0 -0
  123. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_verify_call.py +0 -0
  124. {codecortex-0.17.0 → codecortex-0.18.0}/tests/test_version_skew.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.17.0
3
+ Version: 0.18.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
@@ -279,7 +279,7 @@ Then it launches the exact command it registered and drives a real MCP handshake
279
279
  ```text
280
280
  v claude: registered at /Users/you/.claude.json
281
281
 
282
- v verified: codeintel 0.16.0 — 4 tools (code.query, code.status, code.doctor, code.map)
282
+ v verified: codeintel 0.18.0 — 4 tools (code.query, code.status, code.doctor, code.map)
283
283
  ```
284
284
 
285
285
  If the command is not on `PATH`, or the server fails to start, install says so and exits non-zero
@@ -396,7 +396,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
396
396
  | `codeintel install [--agent auto\|claude\|codex\|gemini\|zed\|all] [--no-verify] [--relative-command]` | Register codeintel with the agents installed on this machine (`auto`, the default), then prove it by completing a real MCP handshake against the registered command |
397
397
  | `codeintel setup [project_root] [--all] [--index] [--warm] [--install-uv] [--install-deps] [--json]` | Prepare backends + index this repo (`--all` = one command: do everything automatable, idempotent); ends with a health report + **Next:** steps |
398
398
  | `codeintel prompt [project_root] [--agent auto\|claude\|codex\|gemini\|zed] [--fresh] [--deep]` | Print a paste-to-your-agent setup prompt; probes health and emits only the outstanding steps (or "just restart me" when already healthy). `--fresh` = the full sequence from `pip install`, to send a friend |
399
- | `codeintel index [project_root]` | Index a project for semantic search |
399
+ | `codeintel index [project_root] [--quiet]` | Index a repo (semantic embeddings + best-effort graph & map refresh), with a live progress display; `--quiet` prints only the result line |
400
400
  | `codeintel serve` | Start the MCP server (stdio transport) |
401
401
  | `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) |
402
402
  | `codeintel query --op OP --target TARGET [--engine auto] [--project-root DIR] [--json]` | Run a single query and print the result |
@@ -404,7 +404,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
404
404
  | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
405
405
  | `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
406
406
  | `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) |
407
- | `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear the semantic index (this repo, or `--all`) to recover from a corrupt/stale DB |
407
+ | `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear this repo's index **both** semantic and graph — so it's as if never indexed; `--all` wipes every repo. Recovers from a corrupt/stale DB |
408
408
  | `codeintel gen-token` | Print a secure random bearer token (for `serve-http` / RBAC `auth.toml`) |
409
409
 
410
410
  Human-facing commands (`doctor`, `setup`, `prompt`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.
@@ -444,7 +444,7 @@ Config is **validated on load** — an out-of-range number, a misspelled enum, o
444
444
  | `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
445
445
  | `CODEINTEL_DEBUG=1` | Log the full traceback of any error the never-throw contract swallows (silent by default) — the switch for diagnosing an unexpected `null` |
446
446
  | `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
447
- | `CODEINTEL_HOME` | Where the index cache lives (default `~/.codeintel`). Set this when the process has **no resolvable home directory** — a container running as a UID with no passwd entry and no `$HOME`, which is common when an agent runs in one. Without it, `Path.home()` raises and every command fails somewhere far from the cause. |
447
+ | `CODEINTEL_HOME` | Where per-machine state lives (default `~/.codeintel`) — the index cache, the global `config.toml`, and `auth.toml`. Set this when the process has **no resolvable home directory** — a container running as a UID with no passwd entry and no `$HOME`, which is common when an agent runs in one. Without it, `Path.home()` raises and every command fails somewhere far from the cause. |
448
448
 
449
449
  ## Privacy & dependencies
450
450
 
@@ -242,7 +242,7 @@ Then it launches the exact command it registered and drives a real MCP handshake
242
242
  ```text
243
243
  v claude: registered at /Users/you/.claude.json
244
244
 
245
- v verified: codeintel 0.16.0 — 4 tools (code.query, code.status, code.doctor, code.map)
245
+ v verified: codeintel 0.18.0 — 4 tools (code.query, code.status, code.doctor, code.map)
246
246
  ```
247
247
 
248
248
  If the command is not on `PATH`, or the server fails to start, install says so and exits non-zero
@@ -359,7 +359,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
359
359
  | `codeintel install [--agent auto\|claude\|codex\|gemini\|zed\|all] [--no-verify] [--relative-command]` | Register codeintel with the agents installed on this machine (`auto`, the default), then prove it by completing a real MCP handshake against the registered command |
360
360
  | `codeintel setup [project_root] [--all] [--index] [--warm] [--install-uv] [--install-deps] [--json]` | Prepare backends + index this repo (`--all` = one command: do everything automatable, idempotent); ends with a health report + **Next:** steps |
361
361
  | `codeintel prompt [project_root] [--agent auto\|claude\|codex\|gemini\|zed] [--fresh] [--deep]` | Print a paste-to-your-agent setup prompt; probes health and emits only the outstanding steps (or "just restart me" when already healthy). `--fresh` = the full sequence from `pip install`, to send a friend |
362
- | `codeintel index [project_root]` | Index a project for semantic search |
362
+ | `codeintel index [project_root] [--quiet]` | Index a repo (semantic embeddings + best-effort graph & map refresh), with a live progress display; `--quiet` prints only the result line |
363
363
  | `codeintel serve` | Start the MCP server (stdio transport) |
364
364
  | `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) |
365
365
  | `codeintel query --op OP --target TARGET [--engine auto] [--project-root DIR] [--json]` | Run a single query and print the result |
@@ -367,7 +367,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
367
367
  | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
368
368
  | `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
369
369
  | `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) |
370
- | `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear the semantic index (this repo, or `--all`) to recover from a corrupt/stale DB |
370
+ | `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear this repo's index **both** semantic and graph — so it's as if never indexed; `--all` wipes every repo. Recovers from a corrupt/stale DB |
371
371
  | `codeintel gen-token` | Print a secure random bearer token (for `serve-http` / RBAC `auth.toml`) |
372
372
 
373
373
  Human-facing commands (`doctor`, `setup`, `prompt`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.
@@ -407,7 +407,7 @@ Config is **validated on load** — an out-of-range number, a misspelled enum, o
407
407
  | `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
408
408
  | `CODEINTEL_DEBUG=1` | Log the full traceback of any error the never-throw contract swallows (silent by default) — the switch for diagnosing an unexpected `null` |
409
409
  | `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
410
- | `CODEINTEL_HOME` | Where the index cache lives (default `~/.codeintel`). Set this when the process has **no resolvable home directory** — a container running as a UID with no passwd entry and no `$HOME`, which is common when an agent runs in one. Without it, `Path.home()` raises and every command fails somewhere far from the cause. |
410
+ | `CODEINTEL_HOME` | Where per-machine state lives (default `~/.codeintel`) — the index cache, the global `config.toml`, and `auth.toml`. Set this when the process has **no resolvable home directory** — a container running as a UID with no passwd entry and no `$HOME`, which is common when an agent runs in one. Without it, `Path.home()` raises and every command fails somewhere far from the cause. |
411
411
 
412
412
  ## Privacy & dependencies
413
413
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.17.0
3
+ Version: 0.18.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
@@ -279,7 +279,7 @@ Then it launches the exact command it registered and drives a real MCP handshake
279
279
  ```text
280
280
  v claude: registered at /Users/you/.claude.json
281
281
 
282
- v verified: codeintel 0.16.0 — 4 tools (code.query, code.status, code.doctor, code.map)
282
+ v verified: codeintel 0.18.0 — 4 tools (code.query, code.status, code.doctor, code.map)
283
283
  ```
284
284
 
285
285
  If the command is not on `PATH`, or the server fails to start, install says so and exits non-zero
@@ -396,7 +396,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
396
396
  | `codeintel install [--agent auto\|claude\|codex\|gemini\|zed\|all] [--no-verify] [--relative-command]` | Register codeintel with the agents installed on this machine (`auto`, the default), then prove it by completing a real MCP handshake against the registered command |
397
397
  | `codeintel setup [project_root] [--all] [--index] [--warm] [--install-uv] [--install-deps] [--json]` | Prepare backends + index this repo (`--all` = one command: do everything automatable, idempotent); ends with a health report + **Next:** steps |
398
398
  | `codeintel prompt [project_root] [--agent auto\|claude\|codex\|gemini\|zed] [--fresh] [--deep]` | Print a paste-to-your-agent setup prompt; probes health and emits only the outstanding steps (or "just restart me" when already healthy). `--fresh` = the full sequence from `pip install`, to send a friend |
399
- | `codeintel index [project_root]` | Index a project for semantic search |
399
+ | `codeintel index [project_root] [--quiet]` | Index a repo (semantic embeddings + best-effort graph & map refresh), with a live progress display; `--quiet` prints only the result line |
400
400
  | `codeintel serve` | Start the MCP server (stdio transport) |
401
401
  | `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) |
402
402
  | `codeintel query --op OP --target TARGET [--engine auto] [--project-root DIR] [--json]` | Run a single query and print the result |
@@ -404,7 +404,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
404
404
  | `codeintel doctor [project_root] [--deep] [--json]` | Diagnose per-engine health + repo index status, with a fix for each gap |
405
405
  | `codeintel map [project_root]` | Generate the `CODE_INTEL.md` orientation file |
406
406
  | `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) |
407
- | `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear the semantic index (this repo, or `--all`) to recover from a corrupt/stale DB |
407
+ | `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear this repo's index **both** semantic and graph — so it's as if never indexed; `--all` wipes every repo. Recovers from a corrupt/stale DB |
408
408
  | `codeintel gen-token` | Print a secure random bearer token (for `serve-http` / RBAC `auth.toml`) |
409
409
 
410
410
  Human-facing commands (`doctor`, `setup`, `prompt`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.
@@ -444,7 +444,7 @@ Config is **validated on load** — an out-of-range number, a misspelled enum, o
444
444
  | `CODEINTEL_HTTP_ACCESS_LOG=1` | One log line per HTTP request (method, path, status, latency) |
445
445
  | `CODEINTEL_DEBUG=1` | Log the full traceback of any error the never-throw contract swallows (silent by default) — the switch for diagnosing an unexpected `null` |
446
446
  | `CODEINTEL_REINDEX=off` | Disable the background reindexer; queries then index inline to stay fresh |
447
- | `CODEINTEL_HOME` | Where the index cache lives (default `~/.codeintel`). Set this when the process has **no resolvable home directory** — a container running as a UID with no passwd entry and no `$HOME`, which is common when an agent runs in one. Without it, `Path.home()` raises and every command fails somewhere far from the cause. |
447
+ | `CODEINTEL_HOME` | Where per-machine state lives (default `~/.codeintel`) — the index cache, the global `config.toml`, and `auth.toml`. Set this when the process has **no resolvable home directory** — a container running as a UID with no passwd entry and no `$HOME`, which is common when an agent runs in one. Without it, `Path.home()` raises and every command fails somewhere far from the cause. |
448
448
 
449
449
  ## Privacy & dependencies
450
450
 
@@ -30,6 +30,7 @@ src/codeintel/mapper.py
30
30
  src/codeintel/metrics.py
31
31
  src/codeintel/onboarding.py
32
32
  src/codeintel/outcome.py
33
+ src/codeintel/paths.py
33
34
  src/codeintel/policy.py
34
35
  src/codeintel/progress.py
35
36
  src/codeintel/provider.py
@@ -104,11 +105,14 @@ tests/test_model_dimension.py
104
105
  tests/test_never_raise.py
105
106
  tests/test_onboarding.py
106
107
  tests/test_rbac.py
108
+ tests/test_redaction_boundary.py
107
109
  tests/test_reindexer.py
108
110
  tests/test_rerank.py
109
111
  tests/test_reset.py
110
112
  tests/test_semantic_provider.py
111
113
  tests/test_source_kind.py
114
+ tests/test_staleness.py
115
+ tests/test_symbol_context.py
112
116
  tests/test_term.py
113
117
  tests/test_treesitter.py
114
118
  tests/test_verify.py
@@ -0,0 +1 @@
1
+ __version__ = "0.18.0"
@@ -26,6 +26,7 @@ import os
26
26
  import pathlib
27
27
  import tomllib # stdlib on Python 3.11+ (the project's minimum)
28
28
 
29
+ from codeintel.paths import codeintel_home
29
30
  from codeintel.policy import TieringPolicy
30
31
 
31
32
  logger = logging.getLogger("codeintel")
@@ -78,7 +79,13 @@ def _auth_config_path() -> pathlib.Path | None:
78
79
  if env:
79
80
  p = pathlib.Path(env)
80
81
  return p if p.is_file() else None
81
- default = pathlib.Path.home() / ".codeintel" / "auth.toml"
82
+ try:
83
+ default = codeintel_home() / "auth.toml"
84
+ except Exception:
85
+ # No CODEINTEL_HOME and no resolvable home. `load_auth` promises never to raise, and its
86
+ # whole contract is "absent config → auth disabled"; a host with nowhere to look is the
87
+ # absent case, not a crash. (Previously this read `Path.home()` inline and raised.)
88
+ return None
82
89
  return default if default.is_file() else None
83
90
 
84
91
 
@@ -4,6 +4,8 @@ import logging
4
4
  import pathlib
5
5
  import tomllib # stdlib since 3.11, which is this package's floor
6
6
 
7
+ from codeintel.paths import codeintel_home
8
+
7
9
  logger = logging.getLogger("codeintel")
8
10
 
9
11
  _DEFAULTS: dict = {
@@ -18,7 +20,12 @@ _DEFAULTS: dict = {
18
20
  "model": "BAAI/bge-small-en-v1.5",
19
21
  "chunk_strategy": "syntax", # syntax-aware (def/class boundaries) vs fixed line windows
20
22
  "rerank": "on", # hybrid lexical+semantic rerank of search results
21
- "rerank_candidates": 30, # cosine candidates fused/re-ranked before returning top-k
23
+ # Cosine candidates retrieved, fused and re-ranked before the top-k is returned. Was 30, but
24
+ # the semantic provider hardcoded its own `_display_k * 6` widening that overrode it, so the
25
+ # effective breadth has always been 60; 30 only ever described behaviour the code didn't have.
26
+ # The provider now honours this key, so the default is set to the value that was really in use
27
+ # — otherwise "fixing" the wiring would have silently narrowed everyone's retrieval by half.
28
+ "rerank_candidates": 60,
22
29
  }
23
30
 
24
31
  # Values restricted to a fixed set — anything else falls back to the default.
@@ -80,12 +87,27 @@ def _coerce(cfg: dict) -> dict:
80
87
  return out
81
88
 
82
89
 
90
+ def _global_config() -> dict:
91
+ """The machine-wide ``config.toml``, or ``{}`` when there isn't one.
92
+
93
+ Resolving the directory can RAISE (no ``CODEINTEL_HOME`` and no resolvable home — a container
94
+ UID with no passwd entry). That used to propagate straight out of ``load_config``: the call
95
+ read ``pathlib.Path.home()`` inline, so it blew up before ``_read_toml``'s handler could see
96
+ it, and every query on such a host died as an opaque ``provider-error``. "There is nowhere to
97
+ look for a global config" is not an error — it means there is no global config."""
98
+ try:
99
+ return _read_toml(codeintel_home() / "config.toml")
100
+ except Exception as exc:
101
+ logger.debug("no resolvable config directory (%s) — using defaults + project config", exc)
102
+ return {}
103
+
104
+
83
105
  def load_config(project_root: str | None = None) -> dict:
84
106
  """Return the merged, validated config: defaults < global < project. Values that fail
85
107
  validation fall back to their default (logged), so a bad config file never breaks a query."""
86
108
  root = pathlib.Path(project_root) if project_root is not None else pathlib.Path.cwd()
87
109
 
88
- global_cfg = _read_toml(pathlib.Path.home() / ".codeintel" / "config.toml")
110
+ global_cfg = _global_config()
89
111
  project_cfg = _read_toml(root / ".codeintel.toml")
90
112
 
91
113
  merged = {**_DEFAULTS, **global_cfg, **project_cfg}
@@ -69,9 +69,24 @@ def open_contained(root_real: str, candidate: str | Path, **kwargs):
69
69
  Raises ``ContainmentError`` rather than returning a sentinel, so a caller that wraps reads in a
70
70
  broad ``except Exception`` cannot quietly downgrade an escape to "file not found" — the escape
71
71
  is logged at WARNING here regardless of what the caller does with the exception.
72
+
73
+ A path that simply DOES NOT EXIST is reported as ``FileNotFoundError``, not as an escape.
74
+ ``contained_path`` answers the single question "may this be read", and a deleted file is a
75
+ legitimate no — but it is also the most ordinary thing that can happen to an indexed file, and
76
+ reporting it as a refusal meant every deletion between indexing and a query logged a WARNING
77
+ asking whether a symlink had been "planted after indexing" and rendered the hit as
78
+ ``[refused: resolves outside the indexed root]``. That is a false security alarm on a routine
79
+ event, and it drowns the real ones. The distinction is made HERE rather than by relaxing
80
+ ``contained_path``, whose ``None`` also drives ``_cleanup_deleted``'s reconciliation of deleted
81
+ files — loosening it there would leave their rows in the index forever.
72
82
  """
73
83
  safe = contained_path(root_real, candidate)
74
84
  if safe is None:
85
+ # lexists(), not exists(): a dangling symlink still EXISTS as a link, and a link whose
86
+ # target vanished is a construct worth refusing loudly rather than calling "not found".
87
+ if not os.path.lexists(candidate):
88
+ logger.debug("indexed file no longer exists: %s", candidate)
89
+ raise FileNotFoundError(str(candidate))
75
90
  logger.warning(
76
91
  "refusing to read %s — it does not resolve to a file inside %s (symlink or hard link "
77
92
  "planted after indexing?)", candidate, root_real,
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
11
11
 
12
12
  from codeintel.containment import contained_path, real_root
13
13
  from codeintel.progress import ProgressSink, _Guard
14
+ from codeintel.semantic_db import MAX_CHUNK_CHARS, chunk_content_hash
14
15
  from codeintel.source_kind import (
15
16
  CODE_EXTS,
16
17
  load_gitattributes_globs,
@@ -26,8 +27,10 @@ logger = logging.getLogger(__name__)
26
27
 
27
28
  # Hard ceiling on the characters embedded for ONE chunk. Line-based splitting cannot bound a
28
29
  # minified or generated single-line file, and the embedder's memory use scales with input size.
29
- # Generous enough that no hand-written function is affected.
30
- _MAX_CHUNK_CHARS = 200_000
30
+ # Generous enough that no hand-written function is affected. Defined beside the schema so the
31
+ # searcher hashes an oversized chunk exactly the way the indexer stored it; re-exported here
32
+ # because this module is where the truncation is actually performed (and warned about).
33
+ _MAX_CHUNK_CHARS = MAX_CHUNK_CHARS
31
34
 
32
35
  # Bytes examined when deciding whether a file is binary. A NUL in the first block is the classic
33
36
  # signal and is what `git` itself uses.
@@ -228,6 +231,9 @@ class Indexer:
228
231
  self._ts_parsers: dict = {} # per-instance tree-sitter parser cache (lang -> parser|None)
229
232
  # Why the last index() failed, for callers that must SHOW a reason rather than log one.
230
233
  self.last_error: str | None = None
234
+ # Rows whose content was unchanged but whose `chunk_end` this pass patched in place
235
+ # (pre-migration NULLs). Counted so the commit can be skipped when there were none.
236
+ self._backfilled = 0
231
237
 
232
238
  def _get_embedder(self):
233
239
  if self._embedder is None:
@@ -457,6 +463,77 @@ class Indexer:
457
463
  spans.extend(members)
458
464
  return spans
459
465
 
466
+ def _symbol_index_py(self, tree: ast.Module, n: int) -> list[tuple[int, int, str]]:
467
+ """``(start, end, name)`` for EVERY def/class in the file, nested ones included.
468
+
469
+ Deliberately a full ``ast.walk`` rather than reusing ``_primary_spans``: that one emits
470
+ only the spans worth embedding (top-level defs, class members) and skips nested inner
471
+ functions, but a chunk can land inside one — and "which symbol am I looking at" should
472
+ answer with the innermost enclosing def, not the outermost. Separate from the span
473
+ pipeline on purpose, so chunk boundaries and symbol attribution stay independent."""
474
+ out: list[tuple[int, int, str]] = []
475
+ for node in ast.walk(tree):
476
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
477
+ lo, hi = self._node_span(node, n)
478
+ out.append((lo, hi, node.name))
479
+ return out
480
+
481
+ @staticmethod
482
+ def _ts_node_name(node) -> str | None:
483
+ """The declared name of a tree-sitter def node, or None when it has no usable one.
484
+
485
+ Most def types carry a ``name`` field. Function-bound `const`/`let` (React components and
486
+ hooks — how most modern TS is written) instead nest the identifier one level down in a
487
+ ``variable_declarator``, so an anonymous default export or an IIFE simply yields None and
488
+ the chunk is attributed to whatever encloses it."""
489
+ try:
490
+ named = node.child_by_field_name("name")
491
+ if named is not None and named.text:
492
+ return named.text.decode("utf-8", "replace")
493
+ if node.type in _TS_DECL_TYPES:
494
+ for child in node.children:
495
+ if child.type == "variable_declarator":
496
+ ident = child.child_by_field_name("name")
497
+ if ident is not None and ident.text:
498
+ return ident.text.decode("utf-8", "replace")
499
+ except Exception as exc:
500
+ logger.debug("tree-sitter node name extraction failed: %s", exc)
501
+ return None
502
+
503
+ def _symbol_index_ts(self, root, lang: str, n: int) -> list[tuple[int, int, str]]:
504
+ """``_symbol_index_py`` for tree-sitter languages: every def node anywhere in the tree.
505
+ Iterative, so deep nesting cannot blow the stack (same reason ``nearest_defs`` is)."""
506
+ all_types = _TS_FUNC_TYPES.get(lang, set()) | _TS_CONTAINER_TYPES.get(lang, set())
507
+ if not all_types:
508
+ return []
509
+ arrow_ok = lang in _TS_ARROW_LANGS
510
+ out: list[tuple[int, int, str]] = []
511
+ stack = [root]
512
+ while stack:
513
+ node = stack.pop()
514
+ is_def = node.type in all_types or (
515
+ arrow_ok and node.type in _TS_DECL_TYPES and _ts_decl_is_function(node)
516
+ )
517
+ if is_def:
518
+ name = self._ts_node_name(node)
519
+ if name:
520
+ out.append((node.start_point[0], self._ts_end_line(node, n), name))
521
+ stack.extend(c for c in node.children if c.is_named)
522
+ return out
523
+
524
+ @staticmethod
525
+ def _enclosing_symbol(index: list[tuple[int, int, str]], start: int) -> str | None:
526
+ """The INNERMOST definition containing line ``start``, or None at module level.
527
+
528
+ Innermost = the latest-starting span that still contains the line, so a method inside a
529
+ class reports the method (the useful answer) rather than the class."""
530
+ best_lo = -1
531
+ best_name = None
532
+ for lo, hi, name in index:
533
+ if lo <= start < hi and lo > best_lo:
534
+ best_lo, best_name = lo, name
535
+ return best_name
536
+
460
537
  def _cover(self, primary: list[tuple[int, int]], n: int) -> list[tuple[int, int]]:
461
538
  """Gapless cover of ``[0, n)`` in file order: window-fill every gap between primary spans
462
539
  and window-split any oversized def. The def-aligned *primary* spans are whole and mutually
@@ -479,13 +556,20 @@ class Indexer:
479
556
  result.extend(self._window_spans(cursor, n))
480
557
  return result
481
558
 
559
+ def _chunk_python_ast_full(
560
+ self, lines: list[str], source: str
561
+ ) -> tuple[list[tuple[int, int]], list[tuple[int, int, str]]]:
562
+ """Spans AND the symbol index from ONE parse. Both derive from the same tree, so they can
563
+ never describe different versions of the file."""
564
+ tree = ast.parse(source)
565
+ n = len(lines)
566
+ return self._cover(self._primary_spans(tree, n), n), self._symbol_index_py(tree, n)
567
+
482
568
  def _chunk_python_ast(self, lines: list[str], source: str) -> list[tuple[int, int]]:
483
569
  """Parse ``source`` → a complete, non-overlapping, def-aligned cover of the file as
484
570
  0-based half-open ``(start, end)`` spans. Raises on parse failure (``SyntaxError`` /
485
571
  ``ValueError`` on NUL bytes / ``RecursionError`` / …) so the caller falls back to windows."""
486
- tree = ast.parse(source)
487
- n = len(lines)
488
- return self._cover(self._primary_spans(tree, n), n)
572
+ return self._chunk_python_ast_full(lines, source)[0]
489
573
 
490
574
  def _get_ts_parser(self, lang: str):
491
575
  """Lazily load + cache (per instance) the tree-sitter parser for a language. Returns None
@@ -563,52 +647,67 @@ class Indexer:
563
647
  emit(node)
564
648
  return spans
565
649
 
566
- def _chunk_treesitter(self, lines: list[str], source: str, lang: str):
567
- """Parse ``source`` with tree-sitter a complete, def-aligned cover, or ``None`` when the
568
- parser is unavailable (caller windows). tree-sitter is error-tolerant, so a syntactically
569
- broken file still yields a partial tree (and thus useful spans) rather than raising."""
650
+ def _chunk_treesitter_full(self, lines: list[str], source: str, lang: str):
651
+ """Spans AND the symbol index from ONE tree-sitter parse, or ``None`` when the parser is
652
+ unavailable (caller windows)."""
570
653
  parser = self._get_ts_parser(lang)
571
654
  if parser is None:
572
655
  return None
573
656
  tree = parser.parse(source.encode("utf-8", errors="replace"))
574
657
  n = len(lines)
575
- return self._cover(self._primary_spans_ts(tree.root_node, lang, n), n)
658
+ root = tree.root_node
659
+ return (
660
+ self._cover(self._primary_spans_ts(root, lang, n), n),
661
+ self._symbol_index_ts(root, lang, n),
662
+ )
663
+
664
+ def _chunk_treesitter(self, lines: list[str], source: str, lang: str):
665
+ """Parse ``source`` with tree-sitter → a complete, def-aligned cover, or ``None`` when the
666
+ parser is unavailable (caller windows). tree-sitter is error-tolerant, so a syntactically
667
+ broken file still yields a partial tree (and thus useful spans) rather than raising."""
668
+ got = self._chunk_treesitter_full(lines, source, lang)
669
+ return None if got is None else got[0]
576
670
 
577
671
  def _spans_for_file(
578
672
  self, filepath: Path, lines: list[str], rel_path: str
579
- ) -> list[tuple[int, int]]:
673
+ ) -> tuple[list[tuple[int, int]], list[tuple[int, int, str]]]:
580
674
  """Choose spans for one file under the syntax strategy: ``ast`` for ``.py``, tree-sitter for
581
675
  the mapped languages (TS/JS/Go/Rust/Java/C/C++), fixed windows for everything else and on
582
- any parse/grammar failure or when tree-sitter isn't installed."""
676
+ any parse/grammar failure or when tree-sitter isn't installed.
677
+
678
+ Returns ``(spans, symbol_index)``. The index is empty for the windowed fallbacks — a file
679
+ we could not parse is a file whose symbols we do not know, and guessing them with a regex
680
+ is how a preview ends up confidently naming the wrong function."""
583
681
  if self.chunk_strategy == "syntax":
584
682
  suffix = filepath.suffix.lower()
585
683
  if suffix == ".py":
586
684
  try:
587
- return self._chunk_python_ast(lines, "".join(lines))
685
+ return self._chunk_python_ast_full(lines, "".join(lines))
588
686
  except Exception as exc:
589
687
  logger.debug("syntax chunking failed for %s (%s) — windowing", rel_path, exc)
590
688
  else:
591
689
  lang = _TS_LANG_BY_EXT.get(suffix)
592
690
  if lang is not None:
593
691
  try:
594
- spans = self._chunk_treesitter(lines, "".join(lines), lang)
595
- if spans is not None:
596
- return spans
692
+ got = self._chunk_treesitter_full(lines, "".join(lines), lang)
693
+ if got is not None:
694
+ return got
597
695
  except Exception as exc:
598
696
  logger.debug("tree-sitter chunking failed for %s (%s) — windowing",
599
697
  rel_path, exc)
600
- return self._window_spans(0, len(lines))
698
+ return self._window_spans(0, len(lines)), []
601
699
 
602
700
  # ---- materialisation -------------------------------------------------------------------
603
701
 
604
702
  def _emit_spans(
605
703
  self,
606
704
  spans: list[tuple[int, int]],
705
+ symbols: list[tuple[int, int, str]],
607
706
  lines: list[str],
608
707
  rel_path: str,
609
708
  project_key: str,
610
709
  conn,
611
- new_chunks: list[tuple[str, str, str, int, str]],
710
+ new_chunks: list[tuple[str, str, str, int, int, str, str | None]],
612
711
  ) -> tuple[set[str], bool]:
613
712
  """Materialise spans into new/changed chunk records — shared by both strategies, so the
614
713
  whitespace-skip, hash-dedup, and per-file cap behave identically. Returns
@@ -651,29 +750,45 @@ class Indexer:
651
750
  chunk_count += 1
652
751
  continue
653
752
  chunk_id = f"{project_key}:{rel_path}:{start}"
654
- content_hash = hashlib.sha256(chunk_text.encode()).hexdigest()[:16]
753
+ content_hash = chunk_content_hash(chunk_text)
754
+ symbol = self._enclosing_symbol(symbols, start)
655
755
  keep_ids.add(chunk_id) # produced this pass — keep even when dedup skips re-embed
656
756
  try:
657
757
  row = conn.execute(
658
- "SELECT content_hash FROM chunk_hashes WHERE chunk_id = ?",
758
+ "SELECT content_hash, chunk_end, chunk_symbol FROM chunk_hashes"
759
+ " WHERE chunk_id = ?",
659
760
  (chunk_id,),
660
761
  ).fetchone()
661
762
  if row and row[0] == content_hash:
763
+ # Unchanged content: the stored embedding is still correct and must NOT be
764
+ # recomputed. But a row written before `chunk_end` existed carries NULL, and
765
+ # the searcher cannot verify staleness without it — so patch the span in
766
+ # place. This is what lets an existing 85k-chunk cache gain verification from
767
+ # one ordinary index pass instead of a full re-embed.
768
+ if row[1] != end or row[2] != symbol:
769
+ conn.execute(
770
+ "UPDATE chunk_hashes SET chunk_end = ?, chunk_symbol = ?"
771
+ " WHERE chunk_id = ?",
772
+ (end, symbol, chunk_id),
773
+ )
774
+ self._backfilled += 1
662
775
  chunk_count += 1
663
776
  continue
664
777
  except Exception as exc:
665
778
  logger.debug("hash check failed for %s: %s", chunk_id, exc)
666
- new_chunks.append((chunk_id, chunk_text, rel_path, start, content_hash))
779
+ new_chunks.append(
780
+ (chunk_id, chunk_text, rel_path, start, end, content_hash, symbol)
781
+ )
667
782
  chunk_count += 1
668
783
  return keep_ids, True
669
784
 
670
785
  def _collect_new_chunks(
671
786
  self, root: Path, project_key: str, project_root_real: str
672
- ) -> list[tuple[str, str, str, int, str]]:
673
- """Walk files; return (chunk_id, text, rel_path, start, hash) for new/changed chunks, and
787
+ ) -> list[tuple[str, str, str, int, int, str, str | None]]:
788
+ """Walk files; return (chunk_id, text, rel_path, start, end, hash, symbol) for new/changed chunks, and
674
789
  reconcile each fully-processed file (dropping rows for chunks it no longer produces)."""
675
790
  conn = self.db.conn()
676
- new_chunks: list[tuple[str, str, str, int, str]] = []
791
+ new_chunks: list[tuple[str, str, str, int, int, str, str | None]] = []
677
792
  files_seen = 0
678
793
 
679
794
  for filepath in self._walk_files(root):
@@ -699,9 +814,9 @@ class Indexer:
699
814
  continue
700
815
 
701
816
  rel_path = str(filepath.relative_to(root))
702
- spans = self._spans_for_file(filepath, lines, rel_path)
817
+ spans, symbols = self._spans_for_file(filepath, lines, rel_path)
703
818
  keep_ids, complete = self._emit_spans(
704
- spans, lines, rel_path, project_key, conn, new_chunks
819
+ spans, symbols, lines, rel_path, project_key, conn, new_chunks
705
820
  )
706
821
  if complete:
707
822
  # Reconcile only a fully-processed file: drop rows for defs/windows it no longer
@@ -716,13 +831,24 @@ class Indexer:
716
831
  # out of scope for the indexer.
717
832
  self.db.delete_file_orphans(project_root_real, rel_path, keep_ids)
718
833
 
834
+ # Commit any in-place `chunk_end` backfills. These ride outside `_embed_and_write`'s
835
+ # per-batch commits, and a pass that finds nothing new to embed returns before reaching
836
+ # them — so without this the backfill is computed and then dropped on every up-to-date
837
+ # repo, which is precisely the case it exists to serve.
838
+ if self._backfilled:
839
+ try:
840
+ conn.commit()
841
+ logger.debug("backfilled chunk_end for %d existing chunks", self._backfilled)
842
+ except Exception as exc:
843
+ logger.warning("committing chunk_end backfill failed: %s", exc)
844
+
719
845
  # One final tick with the true totals so the committed scan line is accurate (the per-file
720
846
  # ticks lag by the current file's chunks). The renderer records this even when throttled.
721
847
  self._progress.scan(files_seen, len(new_chunks))
722
848
  return new_chunks
723
849
 
724
850
  def _embed_and_write(
725
- self, new_chunks: list[tuple[str, str, str, int, str]], project_root_real: str
851
+ self, new_chunks: list[tuple[str, str, str, int, int, str, str | None]], project_root_real: str
726
852
  ) -> int:
727
853
  total = len(new_chunks)
728
854
  # Signal the model-load BEFORE materialising the embedder: on a cold cache _get_embedder()
@@ -744,7 +870,8 @@ class Indexer:
744
870
  logger.warning("embedding batch %d failed: %s", i // batch_size, exc)
745
871
  continue
746
872
 
747
- for j, (chunk_id, _, rel_path, chunk_start, content_hash) in enumerate(batch):
873
+ for j, (chunk_id, _, rel_path, chunk_start, chunk_end, content_hash,
874
+ chunk_symbol) in enumerate(batch):
748
875
  if j >= len(embeddings):
749
876
  break
750
877
  try:
@@ -770,9 +897,10 @@ class Indexer:
770
897
  )
771
898
  conn.execute(
772
899
  "INSERT OR REPLACE INTO chunk_hashes"
773
- "(chunk_id, project_root, file_path, chunk_start, content_hash)"
774
- " VALUES (?, ?, ?, ?, ?)",
775
- (chunk_id, project_root_real, rel_path, chunk_start, content_hash),
900
+ "(chunk_id, project_root, file_path, chunk_start, chunk_end,"
901
+ " content_hash, chunk_symbol) VALUES (?, ?, ?, ?, ?, ?, ?)",
902
+ (chunk_id, project_root_real, rel_path, chunk_start, chunk_end,
903
+ content_hash, chunk_symbol),
776
904
  )
777
905
  embedded_count += 1
778
906
  except Exception as exc:
@@ -808,6 +936,7 @@ class Indexer:
808
936
  return 0
809
937
 
810
938
  project_key = _project_key(project_root_real)
939
+ self._backfilled = 0 # per-pass, so a reused Indexer can't carry a stale count forward
811
940
 
812
941
  self._cleanup_deleted(root, project_root_real)
813
942
 
@@ -0,0 +1,31 @@
1
+ """Where codeintel keeps its per-machine state — one definition, used by every consumer.
2
+
3
+ ``CODEINTEL_HOME`` exists because ``Path.home()`` raises when a process has no resolvable home
4
+ directory: a UID with no passwd entry and no ``$HOME``. That is not exotic for this tool. It is an
5
+ MCP server whose whole purpose is to be launched by coding agents, which are routinely run in
6
+ containers shaped exactly that way.
7
+
8
+ The override used to be honoured by the semantic cache alone. ``config.py`` and ``auth.py`` each
9
+ called ``Path.home()`` directly, so on such a host ``load_config()`` raised ``RuntimeError`` — and
10
+ kept raising with ``CODEINTEL_HOME`` set, because the override never reached them. The escape hatch
11
+ did not work in the one environment it was written for, and the failure surfaced several layers
12
+ away as a generic ``provider-error``. Anything resolving this directory now asks here.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import pathlib
18
+
19
+
20
+ def codeintel_home() -> pathlib.Path:
21
+ """The per-machine state directory (cache, global config, auth).
22
+
23
+ Raises ``RuntimeError`` (from ``Path.home()``) when there is no override and no resolvable home
24
+ — deliberately, so a caller that must NAME the problem can. ``SemanticProvider.probe`` reports
25
+ it with the remediation that actually fixes it; callers for whom a missing file is merely
26
+ "no config" should catch it instead (see ``config.global_config_path``).
27
+ """
28
+ override = os.environ.get("CODEINTEL_HOME", "").strip()
29
+ if override:
30
+ return pathlib.Path(override).expanduser()
31
+ return pathlib.Path.home() / ".codeintel"