codecortex 0.16.0__tar.gz → 0.17.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.
- {codecortex-0.16.0/src/codecortex.egg-info → codecortex-0.17.0}/PKG-INFO +4 -4
- {codecortex-0.16.0 → codecortex-0.17.0}/README.md +3 -3
- {codecortex-0.16.0 → codecortex-0.17.0/src/codecortex.egg-info}/PKG-INFO +4 -4
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codecortex.egg-info/SOURCES.txt +3 -0
- codecortex-0.17.0/src/codeintel/__init__.py +1 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/__main__.py +7 -1
- codecortex-0.17.0/src/codeintel/commands/index.py +144 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/reset.py +2 -2
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/gateway.py +3 -3
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/indexer.py +45 -2
- codecortex-0.17.0/src/codeintel/progress.py +78 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/reset.py +47 -1
- codecortex-0.17.0/src/codeintel/term.py +394 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_cli_commands.py +20 -2
- codecortex-0.17.0/tests/test_index_progress.py +263 -0
- codecortex-0.17.0/tests/test_indexer_binary.py +42 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_reset.py +62 -0
- codecortex-0.16.0/src/codeintel/__init__.py +0 -1
- codecortex-0.16.0/src/codeintel/commands/index.py +0 -76
- codecortex-0.16.0/src/codeintel/term.py +0 -161
- {codecortex-0.16.0 → codecortex-0.17.0}/LICENSE +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/pyproject.toml +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/setup.cfg +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codecortex.egg-info/dependency_links.txt +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codecortex.egg-info/entry_points.txt +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codecortex.egg-info/requires.txt +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codecortex.egg-info/top_level.txt +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/agent_prompt.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/auth.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/cache.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/__init__.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/_common.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/doctor.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/gen_token.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/graph.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/install.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/map.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/prompt.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/query.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/serve.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/serve_http.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/setup.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/commands/status.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/config.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/containment.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/doctor.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/graph_backend.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/graph_render.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/graph_resolution.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/grapher.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/http_server.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/injector.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/installer.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/loc.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/logconfig.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/mapper.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/metrics.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/onboarding.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/outcome.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/policy.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/provider.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/providers/__init__.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/providers/graph.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/providers/lsp.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/providers/none.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/providers/semantic.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/py.typed +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/redact.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/reindexer.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/searcher.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/semantic_db.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/server.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/source_kind.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/verify.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/viewer/__init__.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/src/codeintel/viewer/graph_template.html +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_agent_prompt.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_backend_reaper.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_cache.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_chunking.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_cli_help.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_cold_process.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_config.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_corpus.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_docs_ci_claims.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_docs_deadcode_withdrawal.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_doctor.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_e2e.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_engine_adoption.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_enterprise.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_gateway.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_graph_failure_population.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_graph_provider.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_graph_real.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_graph_stdin.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_grapher.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_hardening.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_http_auth.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_http_server.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_incompleteness.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_installer.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_integration.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_loc_census.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_lsp_provider.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_lsp_real.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_mapper.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_mcp_handshake.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_mcp_server.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_model_dimension.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_never_raise.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_onboarding.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_rbac.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_reindexer.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_rerank.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_semantic_provider.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_source_kind.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_term.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_treesitter.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_verify.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.0}/tests/test_verify_call.py +0 -0
- {codecortex-0.16.0 → codecortex-0.17.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.
|
|
3
|
+
Version: 0.17.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.
|
|
282
|
+
v verified: codeintel 0.16.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
|
|
@@ -407,7 +407,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
407
407
|
| `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear the semantic index (this repo, or `--all`) to recover 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
|
-
Human-facing commands (`doctor`, `
|
|
410
|
+
Human-facing commands (`doctor`, `setup`, `prompt`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.
|
|
411
411
|
|
|
412
412
|
**Exit codes**, so a `make` target or CI step can gate on `$?`:
|
|
413
413
|
|
|
@@ -617,7 +617,7 @@ git clone https://github.com/hamilton-sky/codeintel.git
|
|
|
617
617
|
cd codeintel
|
|
618
618
|
pip install -e .[dev]
|
|
619
619
|
|
|
620
|
-
pytest tests/ -q # ~
|
|
620
|
+
pytest tests/ -q # ~740 tests, ~30s; fails under 83% coverage
|
|
621
621
|
ruff check src tests # lint
|
|
622
622
|
mypy # types (src/ only)
|
|
623
623
|
```
|
|
@@ -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.
|
|
245
|
+
v verified: codeintel 0.16.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
|
|
@@ -370,7 +370,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
370
370
|
| `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear the semantic index (this repo, or `--all`) to recover 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
|
-
Human-facing commands (`doctor`, `
|
|
373
|
+
Human-facing commands (`doctor`, `setup`, `prompt`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.
|
|
374
374
|
|
|
375
375
|
**Exit codes**, so a `make` target or CI step can gate on `$?`:
|
|
376
376
|
|
|
@@ -580,7 +580,7 @@ git clone https://github.com/hamilton-sky/codeintel.git
|
|
|
580
580
|
cd codeintel
|
|
581
581
|
pip install -e .[dev]
|
|
582
582
|
|
|
583
|
-
pytest tests/ -q # ~
|
|
583
|
+
pytest tests/ -q # ~740 tests, ~30s; fails under 83% coverage
|
|
584
584
|
ruff check src tests # lint
|
|
585
585
|
mypy # types (src/ only)
|
|
586
586
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.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.
|
|
282
|
+
v verified: codeintel 0.16.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
|
|
@@ -407,7 +407,7 @@ Full system docs live in [`docs/`](docs/) — start with the index:
|
|
|
407
407
|
| `codeintel reset [project_root] [--all] [--yes] [--json]` | Clear the semantic index (this repo, or `--all`) to recover 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
|
-
Human-facing commands (`doctor`, `
|
|
410
|
+
Human-facing commands (`doctor`, `setup`, `prompt`, `reset`) honor `--no-color` / `NO_COLOR` and `--ascii`, and auto-degrade to plain text when piped.
|
|
411
411
|
|
|
412
412
|
**Exit codes**, so a `make` target or CI step can gate on `$?`:
|
|
413
413
|
|
|
@@ -617,7 +617,7 @@ git clone https://github.com/hamilton-sky/codeintel.git
|
|
|
617
617
|
cd codeintel
|
|
618
618
|
pip install -e .[dev]
|
|
619
619
|
|
|
620
|
-
pytest tests/ -q # ~
|
|
620
|
+
pytest tests/ -q # ~740 tests, ~30s; fails under 83% coverage
|
|
621
621
|
ruff check src tests # lint
|
|
622
622
|
mypy # types (src/ only)
|
|
623
623
|
```
|
|
@@ -31,6 +31,7 @@ src/codeintel/metrics.py
|
|
|
31
31
|
src/codeintel/onboarding.py
|
|
32
32
|
src/codeintel/outcome.py
|
|
33
33
|
src/codeintel/policy.py
|
|
34
|
+
src/codeintel/progress.py
|
|
34
35
|
src/codeintel/provider.py
|
|
35
36
|
src/codeintel/py.typed
|
|
36
37
|
src/codeintel/redact.py
|
|
@@ -89,6 +90,8 @@ tests/test_hardening.py
|
|
|
89
90
|
tests/test_http_auth.py
|
|
90
91
|
tests/test_http_server.py
|
|
91
92
|
tests/test_incompleteness.py
|
|
93
|
+
tests/test_index_progress.py
|
|
94
|
+
tests/test_indexer_binary.py
|
|
92
95
|
tests/test_installer.py
|
|
93
96
|
tests/test_integration.py
|
|
94
97
|
tests/test_loc_census.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.17.0"
|
|
@@ -131,13 +131,19 @@ def main() -> None:
|
|
|
131
131
|
subparsers.add_parser("serve", help="Start the MCP server")
|
|
132
132
|
|
|
133
133
|
# index subcommand
|
|
134
|
-
index_parser = subparsers.add_parser(
|
|
134
|
+
index_parser = subparsers.add_parser(
|
|
135
|
+
"index", parents=[color_parent],
|
|
136
|
+
help="Index a project for semantic search")
|
|
135
137
|
index_parser.add_argument(
|
|
136
138
|
"project_root",
|
|
137
139
|
nargs="?",
|
|
138
140
|
default=None,
|
|
139
141
|
help="Project root directory (default: cwd)",
|
|
140
142
|
)
|
|
143
|
+
index_parser.add_argument(
|
|
144
|
+
"--quiet", "-q", action="store_true",
|
|
145
|
+
help="Suppress the live progress display and header; keep only the result line",
|
|
146
|
+
)
|
|
141
147
|
|
|
142
148
|
# query subcommand
|
|
143
149
|
query_parser = subparsers.add_parser("query", help="Query the code intelligence engine")
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""`codeintel index` — build the semantic index, then best-effort refresh the graph and the map."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from codeintel.commands._common import resolve_root
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _ProgressLogBridge(logging.Handler):
|
|
11
|
+
"""Routes WARNING+ log records from the indexer to a live progress counter's ``notice()``, so a
|
|
12
|
+
per-file skip warning prints as a clean permanent line ABOVE the redrawing status line instead
|
|
13
|
+
of colliding with it (``0 chunksskipping …``). Installed only for a live (TTY) run and removed
|
|
14
|
+
the moment indexing returns — a non-TTY has no live line to protect, and the warnings keep their
|
|
15
|
+
normal stderr path there."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, counter: Any) -> None:
|
|
18
|
+
super().__init__(level=logging.WARNING)
|
|
19
|
+
self._counter = counter
|
|
20
|
+
|
|
21
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
22
|
+
try:
|
|
23
|
+
self._counter.notice(record.getMessage())
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run(args: Any) -> int:
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
import codeintel.term as term
|
|
32
|
+
from codeintel.config import load_config
|
|
33
|
+
from codeintel.indexer import Indexer
|
|
34
|
+
from codeintel.semantic_db import SemanticDb, default_db_path
|
|
35
|
+
|
|
36
|
+
project_root = resolve_root(args)
|
|
37
|
+
# Validate before doing anything. `codeintel index /typo/path` walked nothing, found nothing,
|
|
38
|
+
# and printed "Nothing new to index" at exit 0 — indistinguishable from a correct incremental
|
|
39
|
+
# run, which is the worst possible response to a mistyped path in a script.
|
|
40
|
+
if not os.path.isdir(project_root):
|
|
41
|
+
print(f"index failed: not a directory: {project_root}")
|
|
42
|
+
return 1
|
|
43
|
+
|
|
44
|
+
# A console bound to the CURRENT stdout (honouring --no-color/--ascii/NO_COLOR), so progress is
|
|
45
|
+
# captured under test and routed correctly whether or not main() already ran configure(). The
|
|
46
|
+
# LiveCounter auto-goes silent on a non-TTY; --quiet suppresses it (and the header) outright.
|
|
47
|
+
console = term.Console(
|
|
48
|
+
stream=sys.stdout,
|
|
49
|
+
no_color=getattr(args, "no_color", False),
|
|
50
|
+
ascii_mode=(True if getattr(args, "ascii", False) else None),
|
|
51
|
+
)
|
|
52
|
+
quiet = getattr(args, "quiet", False)
|
|
53
|
+
counter = None if quiet else term.LiveCounter(console)
|
|
54
|
+
|
|
55
|
+
failed = False
|
|
56
|
+
# Wrap the whole semantic pass so a setup failure (e.g. an unresolvable home dir →
|
|
57
|
+
# Path.home() raising) degrades with a message, like every other subcommand, not a traceback.
|
|
58
|
+
try:
|
|
59
|
+
cfg = load_config(project_root)
|
|
60
|
+
if not quiet:
|
|
61
|
+
# One dim header naming the model + chunk strategy: it says WHAT is running (and that it
|
|
62
|
+
# is all local), and on a cold cache it pre-explains the model-download stall.
|
|
63
|
+
strat = str(cfg.get("chunk_strategy", "syntax"))
|
|
64
|
+
strat_label = "def-aligned" if strat == "syntax" else "line-window"
|
|
65
|
+
model = str(cfg.get("model") or "BAAI/bge-small-en-v1.5")
|
|
66
|
+
sep = "." if console.ascii else "·"
|
|
67
|
+
print(console.header("index", project_root))
|
|
68
|
+
print(f" {console.dim(f'{model} {sep} {strat_label} chunks')}")
|
|
69
|
+
print()
|
|
70
|
+
db_path = default_db_path(str(cfg.get("model") or ""))
|
|
71
|
+
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
|
72
|
+
db = SemanticDb(db_path)
|
|
73
|
+
# While a live status line is on screen, the indexer's skip WARNINGs must not print raw to
|
|
74
|
+
# the terminal (they land mid-line). Route them through the counter's notice() for the span
|
|
75
|
+
# of the index pass only; a non-TTY needs no bridge (its status lines end in \n).
|
|
76
|
+
bridge = _ProgressLogBridge(counter) if (counter is not None and counter.live) else None
|
|
77
|
+
idx_logger = logging.getLogger("codeintel")
|
|
78
|
+
try:
|
|
79
|
+
db.init()
|
|
80
|
+
if bridge is not None:
|
|
81
|
+
idx_logger.addHandler(bridge)
|
|
82
|
+
try:
|
|
83
|
+
count = Indexer(
|
|
84
|
+
db,
|
|
85
|
+
model_name=str(cfg.get("model") or "BAAI/bge-small-en-v1.5"),
|
|
86
|
+
window=int(cfg.get("window", 20)),
|
|
87
|
+
stride=int(cfg.get("stride", 10)),
|
|
88
|
+
max_chunks=int(cfg.get("max_chunks", 500)),
|
|
89
|
+
max_total_chunks=int(cfg.get("max_total_chunks", 100000)),
|
|
90
|
+
chunk_strategy=str(cfg.get("chunk_strategy", "syntax")),
|
|
91
|
+
progress=counter,
|
|
92
|
+
).index(project_root)
|
|
93
|
+
finally:
|
|
94
|
+
if bridge is not None:
|
|
95
|
+
idx_logger.removeHandler(bridge)
|
|
96
|
+
# Seal the live line before the plain summary: commit the ✓ row on real work, else erase
|
|
97
|
+
# it so "Nothing new" / the failure message stands on its own clean line.
|
|
98
|
+
if counter:
|
|
99
|
+
counter.finish(commit=count > 0)
|
|
100
|
+
if count > 0:
|
|
101
|
+
print(f"Indexed {count} chunks")
|
|
102
|
+
elif count < 0:
|
|
103
|
+
# Indexer.index() returns -1 for an unrecoverable failure. `> 0` sent that into
|
|
104
|
+
# the "Nothing new to index" branch, so a total failure read as a clean no-op.
|
|
105
|
+
print("index failed — the indexer could not complete (see the warnings above)")
|
|
106
|
+
failed = True
|
|
107
|
+
else:
|
|
108
|
+
print("Nothing new to index")
|
|
109
|
+
finally:
|
|
110
|
+
db.close()
|
|
111
|
+
except Exception as exc:
|
|
112
|
+
if counter:
|
|
113
|
+
counter.finish(commit=False)
|
|
114
|
+
print(f"index failed: {exc}")
|
|
115
|
+
failed = True
|
|
116
|
+
|
|
117
|
+
# best-effort graph reindex — an opaque subprocess, so a ticking elapsed heartbeat is the honest
|
|
118
|
+
# fidelity (no counts cross the boundary): the number keeps moving so a minute-long reindex never
|
|
119
|
+
# reads as a hang, but we never fabricate a percentage we can't see.
|
|
120
|
+
import shutil
|
|
121
|
+
|
|
122
|
+
if shutil.which("codebase-memory-mcp"):
|
|
123
|
+
beat = None if quiet else term.LiveHeartbeat(console, "graph reindex").start()
|
|
124
|
+
try:
|
|
125
|
+
from codeintel.reindexer import Reindexer
|
|
126
|
+
|
|
127
|
+
Reindexer()._graph_reindex(project_root)
|
|
128
|
+
if beat:
|
|
129
|
+
beat.stop("ok")
|
|
130
|
+
except Exception:
|
|
131
|
+
if beat:
|
|
132
|
+
beat.stop("warn", "skipped")
|
|
133
|
+
|
|
134
|
+
# best-effort map refresh after index (fast, kept silent — not worth a checklist row)
|
|
135
|
+
try:
|
|
136
|
+
from codeintel.mapper import MapGenerator
|
|
137
|
+
from codeintel.providers.graph import GraphProvider
|
|
138
|
+
|
|
139
|
+
gen = MapGenerator(GraphProvider())
|
|
140
|
+
gen.write(project_root, gen.generate(project_root))
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
return 1 if failed else 0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""`codeintel reset` — clear
|
|
1
|
+
"""`codeintel reset` — clear a repo's index (semantic + graph), or the whole cache with --all."""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
4
|
from typing import Any
|
|
@@ -18,7 +18,7 @@ def run(args: Any) -> int:
|
|
|
18
18
|
return 1
|
|
19
19
|
target = "ALL projects" if args.all else project_root
|
|
20
20
|
count = preview.get("count", 0)
|
|
21
|
-
ans = input(f"Reset
|
|
21
|
+
ans = input(f"Reset index for {target} ({count} entries)? [y/N] ").strip().lower()
|
|
22
22
|
if ans not in ("y", "yes"):
|
|
23
23
|
print("aborted")
|
|
24
24
|
return 0
|
|
@@ -24,7 +24,6 @@ _AUTO_ENGINE: dict[str, str] = {
|
|
|
24
24
|
"overview": "graph",
|
|
25
25
|
"changed": "graph",
|
|
26
26
|
"changes": "graph",
|
|
27
|
-
"deadcode": "graph",
|
|
28
27
|
"hotspots": "graph",
|
|
29
28
|
"symbol": "lsp",
|
|
30
29
|
"search": "semantic",
|
|
@@ -33,8 +32,9 @@ _AUTO_ENGINE: dict[str, str] = {
|
|
|
33
32
|
|
|
34
33
|
# Ops whose answer depends on live, unhashable state (the git worktree) rather than the indexed
|
|
35
34
|
# content the cache key is built from — caching them would serve a stale answer within a freshness
|
|
36
|
-
# generation. `changed` reads uncommitted edits; `
|
|
37
|
-
#
|
|
35
|
+
# generation. `changed` reads uncommitted edits; `hotspots` is a pure function of the index and
|
|
36
|
+
# stays cached (correctly keyed by the freshness generation). A retired/unknown op falls through
|
|
37
|
+
# to the graph engine (the `_AUTO_ENGINE.get(op, "graph")` default), which safe-nulls it.
|
|
38
38
|
_UNCACHED_OPS: frozenset[str] = frozenset({"changed", "changes"})
|
|
39
39
|
|
|
40
40
|
# Engines whose slot can be filled after construction (see `Gateway.adopt_provider`).
|
|
@@ -10,6 +10,7 @@ from pathlib import Path
|
|
|
10
10
|
from typing import TYPE_CHECKING
|
|
11
11
|
|
|
12
12
|
from codeintel.containment import contained_path, real_root
|
|
13
|
+
from codeintel.progress import ProgressSink, _Guard
|
|
13
14
|
from codeintel.source_kind import (
|
|
14
15
|
CODE_EXTS,
|
|
15
16
|
load_gitattributes_globs,
|
|
@@ -170,6 +171,20 @@ def _looks_binary(path) -> bool:
|
|
|
170
171
|
return True # unreadable → treat as unindexable rather than crash
|
|
171
172
|
|
|
172
173
|
|
|
174
|
+
def _nul_byte_line(path) -> int | None:
|
|
175
|
+
"""1-based line of the first NUL byte within the sniff window, or None. Turns the bare "looks
|
|
176
|
+
binary" skip into a message that points at the exact spot — a raw NUL in a source file is almost
|
|
177
|
+
always a deliberate separator (`.join('\\0')`, a composite key) saved as a byte instead of an
|
|
178
|
+
escape, and naming the line is the difference between a fixable warning and a puzzle."""
|
|
179
|
+
try:
|
|
180
|
+
with open(path, "rb") as fh:
|
|
181
|
+
head = fh.read(_BINARY_SNIFF_BYTES)
|
|
182
|
+
except OSError:
|
|
183
|
+
return None
|
|
184
|
+
i = head.find(b"\x00")
|
|
185
|
+
return None if i < 0 else head.count(b"\n", 0, i) + 1
|
|
186
|
+
|
|
187
|
+
|
|
173
188
|
class Indexer:
|
|
174
189
|
def __init__(
|
|
175
190
|
self,
|
|
@@ -181,9 +196,14 @@ class Indexer:
|
|
|
181
196
|
max_total_chunks: int = 100000,
|
|
182
197
|
chunk_strategy: str = "syntax",
|
|
183
198
|
max_chunk_lines: int | None = None,
|
|
199
|
+
progress: ProgressSink | None = None,
|
|
184
200
|
) -> None:
|
|
185
201
|
self.db = db
|
|
186
202
|
self.model_name = model_name
|
|
203
|
+
# Optional live-progress sink. Wrapped once in _Guard so every emit below is null-safe and
|
|
204
|
+
# never-raise: with the default None it is a no-op (the MCP server and Reindexer get silence
|
|
205
|
+
# and pay nothing), and a misbehaving sink can never abort a pass or change the count.
|
|
206
|
+
self._progress = _Guard(progress)
|
|
187
207
|
# Coerce the numeric knobs defensively: the 4 production call sites pass config-validated
|
|
188
208
|
# values, but a direct caller must not be able to set a stride/window that raises in
|
|
189
209
|
# range() or silently drops regions (mirrors config._coerce's _POSITIVE_INTS clamp).
|
|
@@ -356,7 +376,13 @@ class Indexer:
|
|
|
356
376
|
# garbage — 196KB of /dev/urandom produced 162 chunks — which then competed for
|
|
357
377
|
# rank against real code in every search.
|
|
358
378
|
if _looks_binary(candidate):
|
|
359
|
-
|
|
379
|
+
line = _nul_byte_line(candidate)
|
|
380
|
+
where = f" at line {line}" if line else ""
|
|
381
|
+
logger.warning(
|
|
382
|
+
"skipping %s — contains a null byte%s, so it reads as binary (the rule git "
|
|
383
|
+
"uses too). If that null is intentional (e.g. a key/join separator), write "
|
|
384
|
+
"it as the `\\0` escape instead of a raw byte and re-run index.",
|
|
385
|
+
candidate, where)
|
|
360
386
|
continue
|
|
361
387
|
# Last resort, and the only check that catches a bundle whose directory and
|
|
362
388
|
# filename both look ordinary. One 6.7MB minified chunk contributes hundreds of
|
|
@@ -648,6 +674,7 @@ class Indexer:
|
|
|
648
674
|
reconcile each fully-processed file (dropping rows for chunks it no longer produces)."""
|
|
649
675
|
conn = self.db.conn()
|
|
650
676
|
new_chunks: list[tuple[str, str, str, int, str]] = []
|
|
677
|
+
files_seen = 0
|
|
651
678
|
|
|
652
679
|
for filepath in self._walk_files(root):
|
|
653
680
|
if len(new_chunks) >= self.max_total_chunks:
|
|
@@ -657,6 +684,10 @@ class Indexer:
|
|
|
657
684
|
self.max_total_chunks,
|
|
658
685
|
)
|
|
659
686
|
break
|
|
687
|
+
files_seen += 1
|
|
688
|
+
# A tick per file guarantees liveness even across a run of files that `continue` below
|
|
689
|
+
# (vanished / unreadable), which is exactly when a lone final tick would look stuck.
|
|
690
|
+
self._progress.scan(files_seen, len(new_chunks))
|
|
660
691
|
try:
|
|
661
692
|
with open(filepath, encoding="utf-8", errors="replace") as f:
|
|
662
693
|
lines = f.readlines()
|
|
@@ -685,17 +716,25 @@ class Indexer:
|
|
|
685
716
|
# out of scope for the indexer.
|
|
686
717
|
self.db.delete_file_orphans(project_root_real, rel_path, keep_ids)
|
|
687
718
|
|
|
719
|
+
# One final tick with the true totals so the committed scan line is accurate (the per-file
|
|
720
|
+
# ticks lag by the current file's chunks). The renderer records this even when throttled.
|
|
721
|
+
self._progress.scan(files_seen, len(new_chunks))
|
|
688
722
|
return new_chunks
|
|
689
723
|
|
|
690
724
|
def _embed_and_write(
|
|
691
725
|
self, new_chunks: list[tuple[str, str, str, int, str]], project_root_real: str
|
|
692
726
|
) -> int:
|
|
727
|
+
total = len(new_chunks)
|
|
728
|
+
# Signal the model-load BEFORE materialising the embedder: on a cold cache _get_embedder()
|
|
729
|
+
# downloads the model (minutes), a distinct stall the scan counter can't cover.
|
|
730
|
+
self._progress.load_model()
|
|
693
731
|
embedder = self._get_embedder() # may raise → propagates to index() → returns -1
|
|
694
732
|
conn = self.db.conn()
|
|
695
733
|
embedded_count = 0
|
|
696
734
|
batch_size = 32
|
|
735
|
+
self._progress.embed(0, total) # land on 0% the instant the model is ready
|
|
697
736
|
|
|
698
|
-
for i in range(0,
|
|
737
|
+
for i in range(0, total, batch_size):
|
|
699
738
|
batch = new_chunks[i: i + batch_size]
|
|
700
739
|
texts = [c[1] for c in batch]
|
|
701
740
|
|
|
@@ -744,6 +783,10 @@ class Indexer:
|
|
|
744
783
|
except Exception as exc:
|
|
745
784
|
logger.warning("commit failed after batch %d: %s", i // batch_size, exc)
|
|
746
785
|
|
|
786
|
+
# Report POSITION, not embedded_count: a skipped/failed batch still advanced the work,
|
|
787
|
+
# and a bar that stalls on a `continue` is a lie. Always lands on total at the last batch.
|
|
788
|
+
self._progress.embed(min(i + batch_size, total), total)
|
|
789
|
+
|
|
747
790
|
return embedded_count
|
|
748
791
|
|
|
749
792
|
def _index(self, project_root: str) -> int:
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""The progress-reporting seam for indexing.
|
|
2
|
+
|
|
3
|
+
This is a deliberately tiny, dependency-free contract that lets the indexer say *how far it is*
|
|
4
|
+
without knowing anything about terminals, TTYs, or ANSI. The renderer that turns these calls into
|
|
5
|
+
a live line (``term.LiveCounter``) lives on the far side of this line: it never sees a chunk, and
|
|
6
|
+
the indexer never sees an escape byte. Neither imports the other — the renderer merely *matches*
|
|
7
|
+
``ProgressSink``'s shape.
|
|
8
|
+
|
|
9
|
+
The never-raise guarantee for progress lives in exactly ONE place — ``_Guard``. The indexer calls
|
|
10
|
+
the guard's methods unguarded, so a broken, slow, or half-written sink can never abort an index
|
|
11
|
+
pass or change the count it returns. That single-choke-point is the whole reason this module exists
|
|
12
|
+
rather than the indexer poking a console directly.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Protocol, runtime_checkable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@runtime_checkable
|
|
20
|
+
class ProgressSink(Protocol):
|
|
21
|
+
"""What the indexer emits while it works. Three calls, because the phases genuinely differ:
|
|
22
|
+
|
|
23
|
+
* ``scan`` — the file walk. The file list is a generator, so there is no total; this is a
|
|
24
|
+
running count only (files seen, chunks collected so far).
|
|
25
|
+
* ``load_model`` — a bare marker fired just before the embedding model is materialised. On a
|
|
26
|
+
cold cache that step downloads hundreds of MB and is a *separate* multi-minute stall that
|
|
27
|
+
precedes the first batch; without its own signal it reads as a fresh hang the moment the scan
|
|
28
|
+
counter stops moving.
|
|
29
|
+
* ``embed`` — the batch loop. The total is known up front (``len(new_chunks)``), so this is a
|
|
30
|
+
real ``done``/``total`` and can drive a true percentage.
|
|
31
|
+
|
|
32
|
+
Implementations should not raise, but they are not trusted to honour that: every call the
|
|
33
|
+
indexer makes is routed through :class:`_Guard`.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def scan(self, files: int, chunks: int) -> None: ...
|
|
37
|
+
def load_model(self) -> None: ...
|
|
38
|
+
def embed(self, done: int, total: int) -> None: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _Guard:
|
|
42
|
+
"""Null-safe, never-raise adapter around an optional :class:`ProgressSink`.
|
|
43
|
+
|
|
44
|
+
``Indexer`` holds one of these instead of the raw sink and calls it directly. When the sink is
|
|
45
|
+
``None`` (the default — the MCP server, ``Reindexer``, and every test that doesn't opt in) each
|
|
46
|
+
method is a no-op; when it is present, any exception it throws is swallowed here. This is the
|
|
47
|
+
only spot the contract "progress can never break indexing" is enforced, so it is the only spot
|
|
48
|
+
that needs a test to prove it.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
__slots__ = ("_sink",)
|
|
52
|
+
|
|
53
|
+
def __init__(self, sink: ProgressSink | None) -> None:
|
|
54
|
+
self._sink = sink
|
|
55
|
+
|
|
56
|
+
def scan(self, files: int, chunks: int) -> None:
|
|
57
|
+
if self._sink is None:
|
|
58
|
+
return
|
|
59
|
+
try:
|
|
60
|
+
self._sink.scan(files, chunks)
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def load_model(self) -> None:
|
|
65
|
+
if self._sink is None:
|
|
66
|
+
return
|
|
67
|
+
try:
|
|
68
|
+
self._sink.load_model()
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def embed(self, done: int, total: int) -> None:
|
|
73
|
+
if self._sink is None:
|
|
74
|
+
return
|
|
75
|
+
try:
|
|
76
|
+
self._sink.embed(done, total)
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
import glob
|
|
9
9
|
import logging
|
|
10
10
|
import os
|
|
11
|
+
import re
|
|
11
12
|
import sqlite3
|
|
12
13
|
|
|
13
14
|
import sqlite_vec
|
|
@@ -159,6 +160,44 @@ def _reset_graph_cache(apply: bool) -> dict:
|
|
|
159
160
|
return {"count": count, "corrupt": corrupt}
|
|
160
161
|
|
|
161
162
|
|
|
163
|
+
def _graph_slug(canonical_root: str) -> str:
|
|
164
|
+
"""The stem the graph backend gives a project's cache file: every run of non-alphanumeric
|
|
165
|
+
characters collapses to a single ``-``, with the ends trimmed. So ``/Users/me/aws-resource-tools``
|
|
166
|
+
→ ``Users-me-aws-resource-tools`` and ``/tmp/-Weird/repo`` → ``tmp-Weird-repo`` (NOT
|
|
167
|
+
``tmp--Weird-repo`` — a naive ``/``→``-`` replace leaves a double dash and misses the file,
|
|
168
|
+
silently leaking the graph index a scoped reset was meant to remove). Verified against real
|
|
169
|
+
on-disk caches. The collapse also flattens away every path separator, so the target can never
|
|
170
|
+
escape the cache directory."""
|
|
171
|
+
return re.sub(r"[^A-Za-z0-9]+", "-", canonical_root).strip("-")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _reset_graph_project(project_root: str, apply: bool) -> dict:
|
|
175
|
+
"""Remove ONE project's graph index — the per-project sqlite the backend names ``<slug>.db`` (see
|
|
176
|
+
:func:`_graph_slug`), plus its ``-wal``/``-shm``/``.corrupt`` siblings.
|
|
177
|
+
|
|
178
|
+
This is the graph half of a *scoped* reset. Without it, ``reset <repo>`` cleared only the
|
|
179
|
+
semantic side and left the repo listed and queryable in the graph engine — so "start as if never
|
|
180
|
+
indexed" was true only for ``--all``. ``list_projects`` enumerates these files (``_config.db`` is
|
|
181
|
+
key/value config, not a registry), so deleting them is exactly what makes one project vanish.
|
|
182
|
+
Pure file removal like ``_reset_graph_cache``: it works with the backend down and never hangs."""
|
|
183
|
+
directory = _graph_cache_dir()
|
|
184
|
+
slug = _graph_slug(os.path.realpath(str(project_root)))
|
|
185
|
+
found = 0
|
|
186
|
+
removed = 0
|
|
187
|
+
for suffix in (".db", ".db-wal", ".db-shm", ".db.corrupt"):
|
|
188
|
+
path = os.path.join(directory, slug + suffix)
|
|
189
|
+
if not os.path.isfile(path):
|
|
190
|
+
continue
|
|
191
|
+
found += 1
|
|
192
|
+
if apply:
|
|
193
|
+
try:
|
|
194
|
+
os.remove(path)
|
|
195
|
+
removed += 1
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
return {"slug": slug, "found": found, "removed": removed}
|
|
199
|
+
|
|
200
|
+
|
|
162
201
|
def _looks_unreadable(exc: Exception) -> bool:
|
|
163
202
|
"""Whether *exc* means "this file is not a usable sqlite cache" (as opposed to e.g. a lock)."""
|
|
164
203
|
text = f"{type(exc).__name__}: {exc}".lower()
|
|
@@ -220,15 +259,22 @@ def run_reset(
|
|
|
220
259
|
real = os.path.realpath(str(project_root))
|
|
221
260
|
results = [_reset_scoped(project_root, p, apply) for p in files]
|
|
222
261
|
count = sum(r["count"] for r in results)
|
|
262
|
+
# The graph half of a scoped reset — the piece that was missing, so `reset <repo>` now clears
|
|
263
|
+
# BOTH engines and the repo is genuinely "never indexed", matching what `--all` does globally.
|
|
264
|
+
graph = _reset_graph_project(project_root, apply)
|
|
223
265
|
verb = "removed" if apply else "found"
|
|
224
266
|
# Surface per-file trouble instead of summing counts and synthesizing a success line —
|
|
225
267
|
# discarding these is what made a failed reset indistinguishable from a clean one.
|
|
226
268
|
problems = [r["detail"] for r in results if r.get("error") or r.get("recovered") is not None]
|
|
227
|
-
detail = f"{verb} {count} indexed chunk(s)
|
|
269
|
+
detail = f"{verb} {count} indexed chunk(s)"
|
|
270
|
+
if graph["found"]:
|
|
271
|
+
detail += " and the graph index" if apply else " and a graph index"
|
|
272
|
+
detail += " for this project"
|
|
228
273
|
if problems:
|
|
229
274
|
detail = "\n".join([*problems, detail])
|
|
230
275
|
return {"ok": True, "mode": "scoped", "target": real, "count": count, "applied": bool(apply),
|
|
231
276
|
"detail": detail,
|
|
277
|
+
"graph_found": graph["found"], "graph_removed": graph["removed"],
|
|
232
278
|
"failed": any(r.get("error") for r in results)}
|
|
233
279
|
except Exception as exc:
|
|
234
280
|
return {"ok": True, "applied": apply, "detail": f"reset-error: {type(exc).__name__}: {exc}"}
|