codebrain 0.4.0__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codebrain-0.4.0 → codebrain-0.4.1}/PKG-INFO +1 -1
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/__init__.py +1 -1
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/graph/store.py +14 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/indexer.py +3 -4
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/mcp_server.py +27 -41
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain.egg-info/PKG-INFO +1 -1
- {codebrain-0.4.0 → codebrain-0.4.1}/pyproject.toml +1 -1
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_mcp_server.py +85 -11
- {codebrain-0.4.0 → codebrain-0.4.1}/LICENSE +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/README.md +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/__main__.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/actions/__init__.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/actions/base.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/actions/refactor.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/actions/reviewer.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/actions/test_gen.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/agent_bridge.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/analyzer.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/api.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/api_models.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/architecture.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/cli.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/comprehension.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/config.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/context.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/cross_query.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/cross_registry.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/diff_impact.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/env_migration.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/equivalence.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/export.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/frontend.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/graph/__init__.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/graph/query.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/graph/schema.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/hook_runner.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/hooks.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/kt.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/kt_video.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/llm.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/logging.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/mcp_lifecycle.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/memory/__init__.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/memory/store.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/migration.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/modernize.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/onboard.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/__init__.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/base.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/cobol_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/config_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/csharp_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/dart_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/fortran_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/frontend_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/go_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/java_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/kotlin_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/models.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/mumps_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/plsql_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/python_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/registry.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/rust_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/schema_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/typescript_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/typescript_treesitter.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/parser/vue_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/py.typed +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/resolver.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/rewriter.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/schema_migration.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/settings.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/susa_auth.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/test_gaps.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/test_runner.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/tour.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/ui_migration.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/utils.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/validator.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/watcher/__init__.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain/watcher/file_watcher.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain.egg-info/SOURCES.txt +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain.egg-info/dependency_links.txt +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain.egg-info/entry_points.txt +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain.egg-info/requires.txt +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/codebrain.egg-info/top_level.txt +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/setup.cfg +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_actions.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_agent_bridge.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_analyzer.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_api.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_architecture.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_ci.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_cli.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_comprehension.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_context.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_contracts_real.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_coverage_gaps.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_cross_repo.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_csharp_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_dart_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_dataflow.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_dead_code_confidence.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_diff_impact.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_env_migration.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_equivalence.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_error_recovery.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_export.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_fingerprints.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_frontend.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_gate_battle.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_go_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_hooks.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_indexer.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_infra_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_install.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_integration.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_java_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_jyotishyamitra.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_kotlin_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_kt.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_legacy_parsers.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_llm.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_mcp_lifecycle.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_memory.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_migration.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_modernize.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_multi_project_cli.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_narratives.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_onboard.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_orm_detection.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_output_quality.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_plugin_system.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_production_hardening.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_query.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_real_codebase.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_real_features.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_real_frontend.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_real_repos.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_real_world.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_resolver.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_rewriter.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_rust_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_scale.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_scale_optimizations.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_scale_real.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_schema.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_schema_migration.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_schema_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_settings.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_store.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_test_runner.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_tour.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_translate.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_ts_ast_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_ts_parser_enhanced.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_typescript_parser.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_ui_migration.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_utils.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_validation_narratives.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_validator.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_validator_scenarios.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_vscode_extension.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_watch_validate.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_watcher.py +0 -0
- {codebrain-0.4.0 → codebrain-0.4.1}/tests/test_zoom.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codebrain
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Know what breaks before you break it. Structural knowledge graph for codebases — impact analysis, dead code detection, health scores. No LLM required.
|
|
5
5
|
Author: CodeBrain Contributors
|
|
6
6
|
License: MIT License
|
|
@@ -102,6 +102,20 @@ class GraphStore:
|
|
|
102
102
|
self.conn.execute("DELETE FROM nodes WHERE file_path = ?", (path,))
|
|
103
103
|
self.conn.execute("DELETE FROM files WHERE path = ?", (path,))
|
|
104
104
|
|
|
105
|
+
def remove_files(self, paths: list[str]) -> None:
|
|
106
|
+
"""Delete many files and their nodes/edges in ONE transaction.
|
|
107
|
+
|
|
108
|
+
Per-file transactions take minutes when thousands of rows go stale
|
|
109
|
+
(e.g. a venv that used to be indexed); this takes well under a second.
|
|
110
|
+
"""
|
|
111
|
+
if not paths:
|
|
112
|
+
return
|
|
113
|
+
params = [(p,) for p in paths]
|
|
114
|
+
with self.conn:
|
|
115
|
+
self.conn.executemany("DELETE FROM edges WHERE file_path = ?", params)
|
|
116
|
+
self.conn.executemany("DELETE FROM nodes WHERE file_path = ?", params)
|
|
117
|
+
self.conn.executemany("DELETE FROM files WHERE path = ?", params)
|
|
118
|
+
|
|
105
119
|
def clear_all(self) -> None:
|
|
106
120
|
"""Delete all data from the graph (files, nodes, edges).
|
|
107
121
|
|
|
@@ -542,10 +542,9 @@ def incremental_update(
|
|
|
542
542
|
removed = 0
|
|
543
543
|
errors: list[str] = []
|
|
544
544
|
|
|
545
|
-
for
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
removed += 1
|
|
545
|
+
deleted_rels = [normalize_path(fp, repo_root) for fp in deleted_files]
|
|
546
|
+
store.remove_files(deleted_rels)
|
|
547
|
+
removed = len(deleted_rels)
|
|
549
548
|
|
|
550
549
|
# tree-sitter can hang holding the GIL on Windows — isolate those
|
|
551
550
|
# extensions in a subprocess, same as full_index. A hang here would
|
|
@@ -124,10 +124,10 @@ def _make_store():
|
|
|
124
124
|
import threading as _threading
|
|
125
125
|
import time as _time
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
|
|
127
|
+
# Default 30s: the watcher handles live edits instantly; this gate is the
|
|
128
|
+
# convergence safety net. Measured walk cost is ~2-5s on a midsize repo —
|
|
129
|
+
# a 5s interval would burn most of a core just rescanning.
|
|
130
|
+
FRESHNESS_INTERVAL_SECONDS = float(os.environ.get("CODEBRAIN_FRESHNESS_INTERVAL", "30"))
|
|
131
131
|
|
|
132
132
|
_freshness_lock = _threading.Lock()
|
|
133
133
|
_last_freshness_check: dict[str, float] = {}
|
|
@@ -157,10 +157,13 @@ _FRESHNESS_EXEMPT_TOOLS = frozenset({
|
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
def _ensure_fresh() -> None:
|
|
160
|
-
"""
|
|
160
|
+
"""Schedule a background freshness sync if one is due.
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
NEVER does disk walks, hashing, or parsing on the tool path. v0.4.0
|
|
163
|
+
ran the staleness scan inline and on large or venv-polluted repos the
|
|
164
|
+
walk alone exceeded the tool deadline — every query timed out. The
|
|
165
|
+
contract is: tool calls never wait; the index converges within
|
|
166
|
+
seconds via the background worker (single-flight, throttled per DB).
|
|
164
167
|
"""
|
|
165
168
|
if FRESHNESS_INTERVAL_SECONDS <= 0:
|
|
166
169
|
return
|
|
@@ -172,10 +175,24 @@ def _ensure_fresh() -> None:
|
|
|
172
175
|
now = _time.monotonic()
|
|
173
176
|
if now - _last_freshness_check.get(key, 0.0) < FRESHNESS_INTERVAL_SECONDS:
|
|
174
177
|
return
|
|
178
|
+
# Single-flight: the lock is held for the WHOLE background operation
|
|
179
|
+
# and released by the worker. Without this, slow syncs pile up — a
|
|
180
|
+
# repo with thousands of stale rows spawned a new mega-sync every
|
|
181
|
+
# interval while the first was still running.
|
|
175
182
|
if not _freshness_lock.acquire(blocking=False):
|
|
176
|
-
return #
|
|
183
|
+
return # a sync is already in flight
|
|
184
|
+
_last_freshness_check[key] = now
|
|
185
|
+
_threading.Thread(
|
|
186
|
+
target=_freshness_worker,
|
|
187
|
+
args=(db_path,),
|
|
188
|
+
name="cb-freshness-sync",
|
|
189
|
+
daemon=True,
|
|
190
|
+
).start()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _freshness_worker(db_path: Path) -> None:
|
|
194
|
+
"""Owns _freshness_lock; scans and syncs, then releases."""
|
|
177
195
|
try:
|
|
178
|
-
_last_freshness_check[key] = now
|
|
179
196
|
from codebrain.graph.store import GraphStore
|
|
180
197
|
from codebrain.indexer import incremental_update, scan_stale
|
|
181
198
|
|
|
@@ -184,22 +201,10 @@ def _ensure_fresh() -> None:
|
|
|
184
201
|
changed, deleted = scan_stale(repo_root, store)
|
|
185
202
|
if not changed and not deleted:
|
|
186
203
|
return
|
|
187
|
-
if len(changed) + len(deleted) > FRESHNESS_INLINE_LIMIT:
|
|
188
|
-
_log.info(
|
|
189
|
-
"Freshness: %d files drifted — syncing in background",
|
|
190
|
-
len(changed) + len(deleted),
|
|
191
|
-
)
|
|
192
|
-
_threading.Thread(
|
|
193
|
-
target=_background_freshness_sync,
|
|
194
|
-
args=(repo_root, db_path, changed, deleted),
|
|
195
|
-
name="cb-freshness-sync",
|
|
196
|
-
daemon=True,
|
|
197
|
-
).start()
|
|
198
|
-
return
|
|
199
204
|
result = incremental_update(repo_root, changed, deleted, store)
|
|
200
205
|
if result["files_updated"] or result["files_removed"]:
|
|
201
206
|
_log.info(
|
|
202
|
-
"Freshness: updated %d, removed %d (%.3fs)",
|
|
207
|
+
"Freshness sync: updated %d, removed %d (%.3fs)",
|
|
203
208
|
result["files_updated"],
|
|
204
209
|
result["files_removed"],
|
|
205
210
|
result["elapsed_seconds"],
|
|
@@ -210,25 +215,6 @@ def _ensure_fresh() -> None:
|
|
|
210
215
|
_freshness_lock.release()
|
|
211
216
|
|
|
212
217
|
|
|
213
|
-
def _background_freshness_sync(
|
|
214
|
-
repo_root: Path, db_path: Path, changed: list, deleted: list,
|
|
215
|
-
) -> None:
|
|
216
|
-
try:
|
|
217
|
-
from codebrain.graph.store import GraphStore
|
|
218
|
-
from codebrain.indexer import incremental_update
|
|
219
|
-
|
|
220
|
-
with GraphStore(db_path) as store:
|
|
221
|
-
result = incremental_update(repo_root, changed, deleted, store)
|
|
222
|
-
_log.info(
|
|
223
|
-
"Background freshness sync: updated %d, removed %d (%.3fs)",
|
|
224
|
-
result["files_updated"],
|
|
225
|
-
result["files_removed"],
|
|
226
|
-
result["elapsed_seconds"],
|
|
227
|
-
)
|
|
228
|
-
except Exception as exc:
|
|
229
|
-
_log.warning("Background freshness sync failed: %s", exc)
|
|
230
|
-
|
|
231
|
-
|
|
232
218
|
def _safe_tool(fn): # noqa: ANN001, ANN201
|
|
233
219
|
"""Wrap an MCP tool so it cooperates with MCP cancellation.
|
|
234
220
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codebrain
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Know what breaks before you break it. Structural knowledge graph for codebases — impact analysis, dead code detection, health scores. No LLM required.
|
|
5
5
|
Author: CodeBrain Contributors
|
|
6
6
|
License: MIT License
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codebrain"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "Know what breaks before you break it. Structural knowledge graph for codebases — impact analysis, dead code detection, health scores. No LLM required."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {file = "LICENSE"}
|
|
@@ -624,10 +624,20 @@ class TestMCPBadInput:
|
|
|
624
624
|
|
|
625
625
|
|
|
626
626
|
class TestFreshnessOnRead:
|
|
627
|
-
"""Index drift
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
627
|
+
"""Index drift converges at the tool boundary — answers reflect current
|
|
628
|
+
code within seconds regardless of which background watchers are alive,
|
|
629
|
+
and the tool path itself NEVER waits on disk work."""
|
|
630
|
+
|
|
631
|
+
def _wait_for(self, predicate, timeout=15.0):
|
|
632
|
+
import time
|
|
633
|
+
deadline = time.time() + timeout
|
|
634
|
+
while time.time() < deadline:
|
|
635
|
+
if predicate():
|
|
636
|
+
return True
|
|
637
|
+
time.sleep(0.1)
|
|
638
|
+
return False
|
|
639
|
+
|
|
640
|
+
def test_tool_call_triggers_sync_of_offline_edit(self, mcp_project):
|
|
631
641
|
import codebrain.mcp_server as ms
|
|
632
642
|
repo_root, db_path = mcp_project
|
|
633
643
|
ms._last_freshness_check.clear()
|
|
@@ -636,24 +646,62 @@ class TestFreshnessOnRead:
|
|
|
636
646
|
"def hotfix_func():\n return 1\n"
|
|
637
647
|
)
|
|
638
648
|
from codebrain.mcp_server import search_symbol
|
|
639
|
-
result = _run_async(search_symbol("hotfix_func"))
|
|
640
|
-
data = json.loads(result)
|
|
641
|
-
assert len(data) > 0, "freshness gate should have indexed the new file"
|
|
642
649
|
|
|
643
|
-
|
|
650
|
+
def _found():
|
|
651
|
+
data = json.loads(_run_async(search_symbol("hotfix_func")))
|
|
652
|
+
return len(data) > 0
|
|
653
|
+
|
|
654
|
+
assert self._wait_for(_found), (
|
|
655
|
+
"freshness worker should have indexed the offline edit within seconds"
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
def test_tool_path_does_no_disk_scan_inline(self, mcp_project, monkeypatch):
|
|
659
|
+
"""THE v0.4.0 regression: the staleness scan ran inline and on big
|
|
660
|
+
repos the walk alone blew the tool deadline. The tool path must
|
|
661
|
+
return without ever calling scan_stale in the calling thread."""
|
|
662
|
+
import threading
|
|
644
663
|
import codebrain.indexer as idx
|
|
645
664
|
import codebrain.mcp_server as ms
|
|
646
|
-
|
|
665
|
+
scan_threads = []
|
|
647
666
|
real = idx.scan_stale
|
|
648
667
|
monkeypatch.setattr(
|
|
649
668
|
idx, "scan_stale",
|
|
650
|
-
lambda *a, **k: (
|
|
669
|
+
lambda *a, **k: (scan_threads.append(threading.current_thread().name), real(*a, **k))[1],
|
|
651
670
|
)
|
|
652
671
|
ms._last_freshness_check.clear()
|
|
653
672
|
from codebrain.mcp_server import search_symbol
|
|
654
673
|
_run_async(search_symbol("process"))
|
|
674
|
+
self._wait_for(lambda: len(scan_threads) >= 1)
|
|
675
|
+
assert scan_threads, "freshness worker never ran"
|
|
676
|
+
for name in scan_threads:
|
|
677
|
+
assert name == "cb-freshness-sync", (
|
|
678
|
+
f"scan_stale ran on thread {name!r} — disk walks are forbidden "
|
|
679
|
+
"on the tool path"
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
def test_syncs_do_not_pile_up(self, mcp_project, monkeypatch):
|
|
683
|
+
"""Single-flight: while one sync is in flight, new tool calls must
|
|
684
|
+
not spawn additional syncs (mega-syncs used to stack every 5s)."""
|
|
685
|
+
import time
|
|
686
|
+
import codebrain.indexer as idx
|
|
687
|
+
import codebrain.mcp_server as ms
|
|
688
|
+
calls = []
|
|
689
|
+
|
|
690
|
+
def slow_scan(*a, **k):
|
|
691
|
+
calls.append(1)
|
|
692
|
+
time.sleep(1.0)
|
|
693
|
+
return [], []
|
|
694
|
+
|
|
695
|
+
monkeypatch.setattr(idx, "scan_stale", slow_scan)
|
|
696
|
+
monkeypatch.setattr(ms, "FRESHNESS_INTERVAL_SECONDS", 0.01)
|
|
697
|
+
ms._last_freshness_check.clear()
|
|
698
|
+
from codebrain.mcp_server import search_symbol
|
|
655
699
|
_run_async(search_symbol("process"))
|
|
656
|
-
|
|
700
|
+
time.sleep(0.1) # worker now inside slow_scan
|
|
701
|
+
_run_async(search_symbol("process"))
|
|
702
|
+
_run_async(search_symbol("process"))
|
|
703
|
+
time.sleep(1.2) # let the worker finish
|
|
704
|
+
assert len(calls) == 1, "concurrent syncs piled up"
|
|
657
705
|
|
|
658
706
|
def test_validators_are_exempt(self):
|
|
659
707
|
import codebrain.mcp_server as ms
|
|
@@ -661,3 +709,29 @@ class TestFreshnessOnRead:
|
|
|
661
709
|
# freshness sync first would erase the baseline and kill the gate.
|
|
662
710
|
for tool in ("validate_after_write", "validate_change", "propose_change"):
|
|
663
711
|
assert tool in ms._FRESHNESS_EXEMPT_TOOLS
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
class TestBatchedDeletion:
|
|
715
|
+
def test_thousands_of_stale_rows_removed_fast(self, tmp_path):
|
|
716
|
+
"""Stale-row cleanup at venv scale must be sub-second, not minutes."""
|
|
717
|
+
import time
|
|
718
|
+
from codebrain.graph.store import GraphStore
|
|
719
|
+
from codebrain.indexer import incremental_update
|
|
720
|
+
repo = tmp_path / "repo"
|
|
721
|
+
repo.mkdir()
|
|
722
|
+
db = repo / ".codebrain" / "graph.db"
|
|
723
|
+
with GraphStore(db) as store:
|
|
724
|
+
# Fabricate 5000 indexed files that no longer exist on disk
|
|
725
|
+
store.conn.executemany(
|
|
726
|
+
"INSERT INTO files (path, content_hash, last_indexed, line_count) "
|
|
727
|
+
"VALUES (?, 'x', 0, 1)",
|
|
728
|
+
[(f"stale/venv/mod_{i}.py",) for i in range(5000)],
|
|
729
|
+
)
|
|
730
|
+
store.conn.commit()
|
|
731
|
+
ghosts = [repo / f"stale/venv/mod_{i}.py" for i in range(5000)]
|
|
732
|
+
t0 = time.perf_counter()
|
|
733
|
+
result = incremental_update(repo, [], ghosts, store)
|
|
734
|
+
elapsed = time.perf_counter() - t0
|
|
735
|
+
assert result["files_removed"] == 5000
|
|
736
|
+
assert elapsed < 5.0, f"batched deletion took {elapsed:.1f}s"
|
|
737
|
+
assert store.all_file_paths() == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|