webbee 0.7.0__tar.gz → 0.7.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {webbee-0.7.0 → webbee-0.7.2}/CHANGELOG.md +24 -0
- {webbee-0.7.0 → webbee-0.7.2}/PKG-INFO +3 -3
- {webbee-0.7.0 → webbee-0.7.2}/pyproject.toml +3 -3
- webbee-0.7.2/src/webbee/__init__.py +1 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/boot.py +11 -1
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/query.py +90 -3
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/service.py +18 -5
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/tools.py +6 -2
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_any_folder.py +21 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_query.py +30 -1
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_tools.py +16 -1
- webbee-0.7.0/src/webbee/__init__.py +0 -1
- {webbee-0.7.0 → webbee-0.7.2}/.github/workflows/publish.yml +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/.gitignore +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/LICENSE +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/README.md +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/install.sh +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/__main__.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/_launch.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/account.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/active_sessions.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/autoupdate.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/banner_art.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/browser.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/checkpoints.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/cli.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/clipboard.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/clipboard_read.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/clipboard_session.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/coding_context.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/commands.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/config.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/consent.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/details.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/events.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/frames.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/home.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/home_view.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/http.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/instance_lock.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/graph.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/outline.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/store.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/tree.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/watch.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/media.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/modals.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/mode_store.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/native_files.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/native_read.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/newtab_mode.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/output_pane.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/pricing_constants.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/queue_panel.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/reflow.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/remote.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/render.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/repl.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/repo.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/selection.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/session.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/sessions.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/sizing.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/slots.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/steer.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/stream.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/tab_store.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/tabs.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/thread.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/tier_store.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/todo_panel.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/tokens.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/tui.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/update.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/urlopen.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/wallet.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/src/webbee/worktrees.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/__init__.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/conftest.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_account.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_active_sessions.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_audit_2026_07_25_findings.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_autoupdate.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_checkpoints.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_checkpoints_nested_1030.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_cli.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_clipboard.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_clipboard_read.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_code_map_fresh.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_coding_context.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_commands.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_config.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_cpc_outline.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_cross_loop_token.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_details.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_events.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_freeze_fix.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_home.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_home_view.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_home_view_mouse.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_inspect_page.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_instance_lock.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_cache_and_sync_worker.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_checksum_tree.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_chunker.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_embed.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_graph.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_service.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_store.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_vectors.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_watch.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_launch.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_marathon.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_modals_e2e.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_mode_and_remote_0_6_8.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_mode_store.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_native_files.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_newtab_mode.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_packaging.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_probe_cost.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_queue_manage_0337.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_reflow.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_remote_code_reads.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_render.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_repl.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_repo.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_resilient_http.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_review_fixes_1030.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_session.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_session_review_1030.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_sessions.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_sizing.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_slots.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_steer.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_stream.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_tab_bar_stability.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_tab_restore_0337.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_tabs.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_terminal_ux_0336.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_thread.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_tier_invalidation.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_tier_store.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_todo_panel.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_tokens.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_tools_eol_cancel_1030.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_tools_vision_websearch.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_tui.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_tui_hardening.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_update.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_update_badge_0340.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_urlopen.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_version.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_version_badge_corner.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_wallet.py +0 -0
- {webbee-0.7.0 → webbee-0.7.2}/tests/test_worktrees.py +0 -0
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.2
|
|
4
|
+
|
|
5
|
+
* **The code graph answers correctly** (06.11 report):
|
|
6
|
+
* `impact_of_change` lists the files that really use a symbol, with the line, the user and the depth. A
|
|
7
|
+
function nobody calls had answered 481 unrelated files.
|
|
8
|
+
* Files that only share the name are listed apart under `by_name`.
|
|
9
|
+
* An unknown symbol is reported as such, never as "0 users".
|
|
10
|
+
* **Every major language is linked**:
|
|
11
|
+
* Go, Rust, Java, Kotlin, C#, PHP, Ruby;
|
|
12
|
+
* TS/JS monorepos (workspace packages, tsconfig `extends`, default exports, CommonJS, `.mts/.cts`);
|
|
13
|
+
* Vue, Svelte and Astro components;
|
|
14
|
+
* members through typed fields, constructors and return types.
|
|
15
|
+
* **Projects of every ecosystem** are recognised (`Cargo.toml`, `go.mod`, `pom.xml`, `*.csproj`, `composer.json`,
|
|
16
|
+
`Gemfile` ...), locally and on servers.
|
|
17
|
+
* `.gitignore` rules apply in folders without git and on machines without a git binary (Windows).
|
|
18
|
+
* Needs imperal-mcp 0.9.2 and imperal-sdk 6.5.0.
|
|
19
|
+
|
|
20
|
+
## 0.7.1
|
|
21
|
+
|
|
22
|
+
* **A missing code index says why**: when the index did not start (a failure while building it, or it is turned off),
|
|
23
|
+
`impact_of_change`, `graph_slice` and the other code-map tools now name the real reason instead of the wrong
|
|
24
|
+
"install webbee[intel]" -- the index has shipped in the base install since 0.7.0, there is nothing to install.
|
|
25
|
+
* Needs imperal-mcp 0.9.1 (install commands that work on Debian 12+, Ubuntu 23.04+ and Homebrew: `pipx` / `uv tool`).
|
|
26
|
+
|
|
3
27
|
## 0.7.0
|
|
4
28
|
|
|
5
29
|
* **The local index takes any folder and every file kind**: open webbee in a home folder, a folder of contracts or a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: webbee
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Webbee 🐝 — the Imperal Cloud coding agent in your terminal
|
|
5
5
|
Project-URL: Homepage, https://imperal.io
|
|
6
6
|
Project-URL: Documentation, https://docs.imperal.io
|
|
@@ -31,8 +31,8 @@ Requires-Python: >=3.7
|
|
|
31
31
|
Requires-Dist: asyncssh>=2.14; python_version >= '3.10'
|
|
32
32
|
Requires-Dist: httpx-sse>=0.4; python_version >= '3.10'
|
|
33
33
|
Requires-Dist: httpx>=0.27; python_version >= '3.10'
|
|
34
|
-
Requires-Dist: imperal-mcp>=0.9.
|
|
35
|
-
Requires-Dist: imperal-sdk[universal-index]>=6.
|
|
34
|
+
Requires-Dist: imperal-mcp>=0.9.2; python_version >= '3.10'
|
|
35
|
+
Requires-Dist: imperal-sdk[universal-index]>=6.5.0; python_version >= '3.10'
|
|
36
36
|
Requires-Dist: model2vec>=0.8.2; python_version >= '3.10' and (sys_platform != 'darwin' or platform_machine != 'x86_64' or python_version < '3.13')
|
|
37
37
|
Requires-Dist: numpy>=2; python_version >= '3.10' and (sys_platform != 'darwin' or platform_machine != 'x86_64' or python_version < '3.13')
|
|
38
38
|
Requires-Dist: prompt-toolkit>=3; python_version >= '3.10'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "webbee"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.2"
|
|
4
4
|
description = "Webbee 🐝 — the Imperal Cloud coding agent in your terminal"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "GPL-3.0-or-later"
|
|
@@ -13,8 +13,8 @@ dependencies = [
|
|
|
13
13
|
# release on a Python 3.12 it sets up once with uv -- `uv` comes as a wheel on 3.8+,
|
|
14
14
|
# 3.7 fetches it with uv's own installer.
|
|
15
15
|
'uv>=0.4; python_version >= "3.8" and python_version < "3.10"',
|
|
16
|
-
'imperal-mcp>=0.9.
|
|
17
|
-
'imperal-sdk[universal-index]>=6.
|
|
16
|
+
'imperal-mcp>=0.9.2; python_version >= "3.10"', # raised to the newest at each release
|
|
17
|
+
'imperal-sdk[universal-index]>=6.5.0; python_version >= "3.10"',
|
|
18
18
|
'asyncssh>=2.14; python_version >= "3.10"',
|
|
19
19
|
'httpx>=0.27; python_version >= "3.10"',
|
|
20
20
|
'httpx-sse>=0.4; python_version >= "3.10"',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.2"
|
|
@@ -7,6 +7,11 @@ import asyncio
|
|
|
7
7
|
import os
|
|
8
8
|
import subprocess
|
|
9
9
|
|
|
10
|
+
# Why the code index is NOT running in this process ("" while it runs or before the boot). The boot swallows the
|
|
11
|
+
# failure so the REPL never crashes -- but a tool that needs the index must be able to say the real reason instead of
|
|
12
|
+
# guessing ("install webbee[intel]" was wrong since 0.7.0: intel ships in the base install).
|
|
13
|
+
INTEL_DOWN_REASON = ""
|
|
14
|
+
|
|
10
15
|
|
|
11
16
|
def _git_branch(workspace: str) -> str:
|
|
12
17
|
try:
|
|
@@ -105,6 +110,7 @@ async def start_intel(cfg, workspace: str, intel_factory):
|
|
|
105
110
|
failure here (missing extra, bad repo, etc.) must degrade to
|
|
106
111
|
(None, None), never crash the boot -- coding still works, just
|
|
107
112
|
without repo intelligence. Returns (intel, watcher_task)."""
|
|
113
|
+
global INTEL_DOWN_REASON
|
|
108
114
|
try:
|
|
109
115
|
svc = (intel_factory or _default_intel_factory)(cfg, workspace)
|
|
110
116
|
await asyncio.to_thread(svc.build)
|
|
@@ -118,8 +124,10 @@ async def start_intel(cfg, workspace: str, intel_factory):
|
|
|
118
124
|
# its end is always collected: an exception is never printed when webbee quits ("Task exception was never
|
|
119
125
|
# retrieved", live 2026-10-25)
|
|
120
126
|
watcher_task.add_done_callback(lambda t: t.cancelled() or t.exception())
|
|
127
|
+
INTEL_DOWN_REASON = ""
|
|
121
128
|
return svc, watcher_task
|
|
122
|
-
except Exception:
|
|
129
|
+
except Exception as exc:
|
|
130
|
+
INTEL_DOWN_REASON = f"the code index failed to start: {type(exc).__name__}: {str(exc)[:200]}"
|
|
123
131
|
return None, None
|
|
124
132
|
|
|
125
133
|
|
|
@@ -155,10 +163,12 @@ async def boot_workspace(cfg, workspace: str, intel_factory, shadow_factory) ->
|
|
|
155
163
|
behavior per job (a crash in one never blocks the others or the boot),
|
|
156
164
|
just concurrent instead of serial."""
|
|
157
165
|
async def _intel_job():
|
|
166
|
+
global INTEL_DOWN_REASON
|
|
158
167
|
if cfg.intel_enabled:
|
|
159
168
|
# Guarded off-loop build + watcher; any failure degrades to
|
|
160
169
|
# intel=None, never crashes the boot (start_intel above).
|
|
161
170
|
return await start_intel(cfg, workspace, intel_factory)
|
|
171
|
+
INTEL_DOWN_REASON = "the code index is turned off in this configuration (intel_enabled = false)"
|
|
162
172
|
return None, None
|
|
163
173
|
|
|
164
174
|
(intel, watcher_task), shadow, git_branch = await asyncio.gather(
|
|
@@ -186,14 +186,101 @@ def contract_slice(svc, query: str = "", kind: str = "all", k: int = 50) -> dict
|
|
|
186
186
|
return _env(items[:limit], has_more=len(items) > limit)
|
|
187
187
|
|
|
188
188
|
|
|
189
|
+
# The local answer of ``impact_of_change`` computes links the way the vault's graph does (imperal_sdk.code_links,
|
|
190
|
+
# 2026-11-06): before, a file "depended" on a symbol when the bare NAME appeared in it, and each depth step pulled in
|
|
191
|
+
# every definition of every hit file -- a function nobody calls answered 481 files (06.11 report).
|
|
192
|
+
# which files carry scope facts and which are resolution configs: the SDK's one answer (Python, TS/JS, Vue/Svelte/Astro,
|
|
193
|
+
# Go, Rust, Java, Kotlin, C#, PHP, Ruby; go.mod, Cargo.toml, tsconfig, package.json ...) -- never a list of our own
|
|
194
|
+
_FACTS: dict = {} # (root, path, content_hash) -> BlobFacts | None: a file parsed once per content
|
|
195
|
+
_FACTS_MAX = 60000
|
|
196
|
+
_READ_MAX = 2_000_000
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _read(root: str, rel: str):
|
|
200
|
+
import os
|
|
201
|
+
try:
|
|
202
|
+
full = os.path.join(root, rel)
|
|
203
|
+
if os.path.getsize(full) > _READ_MAX:
|
|
204
|
+
return None
|
|
205
|
+
with open(full, encoding="utf-8", errors="replace") as fh:
|
|
206
|
+
return fh.read()
|
|
207
|
+
except OSError:
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _is_config(rel: str) -> bool:
|
|
212
|
+
from imperal_sdk.code_index import is_resolution_config
|
|
213
|
+
return is_resolution_config(rel)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def linker(svc):
|
|
217
|
+
"""The repo's resolver (``imperal_sdk.code_links.Linker``) for the index's current graph -- built once per graph
|
|
218
|
+
(a rebuilt graph builds it again), each file parsed once per content."""
|
|
219
|
+
import os
|
|
220
|
+
g = svc.graph
|
|
221
|
+
if getattr(svc, "_linker_graph", None) is g and getattr(svc, "_linker", None) is not None:
|
|
222
|
+
return svc._linker
|
|
223
|
+
from imperal_sdk import code_index as ci
|
|
224
|
+
from imperal_sdk.code_links import Linker
|
|
225
|
+
root = str(getattr(svc, "root", "") or "")
|
|
226
|
+
if len(_FACTS) > _FACTS_MAX:
|
|
227
|
+
_FACTS.clear()
|
|
228
|
+
facts: dict = {}
|
|
229
|
+
for path, fi in list(svc.index.files.items()):
|
|
230
|
+
if not ci.has_scope_facts(path):
|
|
231
|
+
continue
|
|
232
|
+
key = (root, path, getattr(fi, "content_hash", ""))
|
|
233
|
+
if key not in _FACTS:
|
|
234
|
+
text = _read(root, path) if root else None
|
|
235
|
+
_FACTS[key] = ci.scope_facts(path, text) if text is not None else None
|
|
236
|
+
facts[path] = _FACTS[key]
|
|
237
|
+
configs = []
|
|
238
|
+
cands = {p for p in svc.index.files if _is_config(p)}
|
|
239
|
+
if root:
|
|
240
|
+
cands |= {n for n in os.listdir(root) if _is_config(n)} if os.path.isdir(root) else set()
|
|
241
|
+
for rel in sorted(cands):
|
|
242
|
+
text = _read(root, rel) if root else None
|
|
243
|
+
if text is None:
|
|
244
|
+
continue
|
|
245
|
+
try:
|
|
246
|
+
kind, data = ci.parse_config(rel, text)
|
|
247
|
+
except Exception:
|
|
248
|
+
continue
|
|
249
|
+
configs.append({"path": rel.replace("\\", "/"), "kind": kind, "data": data})
|
|
250
|
+
lk = Linker(facts, configs)
|
|
251
|
+
svc._linker, svc._linker_graph = lk, g
|
|
252
|
+
return lk
|
|
253
|
+
|
|
254
|
+
|
|
189
255
|
def impact_of_change(svc, symbols) -> dict:
|
|
256
|
+
"""Files that use ``symbols`` (resolved links: line, user, depth through the enclosing definition), code before
|
|
257
|
+
tests; ``by_name`` -- files that only share a name (a method on a local, a language without scope facts), marked
|
|
258
|
+
and bounded; ``resolved`` -- the definitions found (none: "not a definition in this index", never "0 users")."""
|
|
190
259
|
if svc is None or svc.graph is None:
|
|
191
260
|
return {"ok": False, "content": "index not ready"}
|
|
192
261
|
if isinstance(symbols, str):
|
|
193
262
|
symbols = [symbols]
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
263
|
+
from imperal_sdk.code_index import has_scope_facts
|
|
264
|
+
from imperal_sdk.code_links import BY_NAME_SHOWN, impact, is_test_path
|
|
265
|
+
syms = [str(s) for s in (symbols or []) if str(s).strip()]
|
|
266
|
+
out = impact(linker(svc), syms, depth=2)
|
|
267
|
+
# languages without scope facts: their uses are known by name only -- listed with the other by-name files
|
|
268
|
+
names = {s.split("::")[-1].split(":")[-1].rsplit(".", 1)[-1] for s in syms}
|
|
269
|
+
known = {b["path"] for b in out["by_name"]} | {i["id"] for i in out["data"]["items"]}
|
|
270
|
+
extra = []
|
|
271
|
+
for path, fi in svc.index.files.items():
|
|
272
|
+
if has_scope_facts(path) or path in known:
|
|
273
|
+
continue
|
|
274
|
+
if names & set(getattr(fi, "refs", ()) or ()):
|
|
275
|
+
extra.append({"path": path, "line": 0, "name": "", "group": "tests" if is_test_path(path) else "code",
|
|
276
|
+
"note": "no scope facts for this language: by name"})
|
|
277
|
+
if extra:
|
|
278
|
+
out["by_name_count"] = int(out.get("by_name_count") or 0) + len(extra)
|
|
279
|
+
out["by_name"] = sorted(out["by_name"] + extra, key=lambda b: (b["group"] == "tests", b["path"]))[:BY_NAME_SHOWN]
|
|
280
|
+
if not out["resolved"]:
|
|
281
|
+
out["note"] = f"{', '.join(syms)}: not a definition in this index"
|
|
282
|
+
out["link"] = "resolved locally (the vault's rules); by_name lists name-only matches apart"
|
|
283
|
+
return out
|
|
197
284
|
|
|
198
285
|
|
|
199
286
|
def orient(svc, q) -> dict:
|
|
@@ -171,10 +171,22 @@ def _index_listing(root: str, focus: str | None = None, limit: int = UNIT_MAX_FI
|
|
|
171
171
|
skipped = {"placeholder": 0, "over_cap": 0}
|
|
172
172
|
excluded = {str(e).strip("/") for e in exclude if str(e).strip("/")}
|
|
173
173
|
listed = _git_listed(root)
|
|
174
|
+
# no git answer (a folder that is no repository, the repositories inside it, a machine without git -- Windows,
|
|
175
|
+
# a minimal Linux): every .gitignore on the way applies, read here (06.11 report: none applied, ~19.6k dump and
|
|
176
|
+
# screenshot files were indexed). The same parser as the SFTP listing (imperal_sdk.code_index.gitignore_rules).
|
|
177
|
+
from imperal_sdk.code_index import gitignore_rules, gitignored
|
|
178
|
+
rules: list | None = None if listed is not None else []
|
|
174
179
|
for dp, dns, fns in os.walk(root):
|
|
175
180
|
here = os.path.relpath(dp, root).replace(os.sep, "/")
|
|
181
|
+
if rules is not None and ".gitignore" in fns:
|
|
182
|
+
try:
|
|
183
|
+
with open(os.path.join(dp, ".gitignore"), encoding="utf-8", errors="replace") as fh:
|
|
184
|
+
rules = rules + gitignore_rules(fh.read(), "" if here == "." else here)
|
|
185
|
+
except OSError:
|
|
186
|
+
pass
|
|
176
187
|
dns[:] = sorted(d for d in dns if not skipped_dir(dp, d)
|
|
177
|
-
and (d if here == "." else f"{here}/{d}") not in excluded
|
|
188
|
+
and (d if here == "." else f"{here}/{d}") not in excluded
|
|
189
|
+
and not (rules and gitignored(rules, d if here == "." else f"{here}/{d}", True)))
|
|
178
190
|
for fn in sorted(fns):
|
|
179
191
|
rel = fn if here == "." else f"{here}/{fn}"
|
|
180
192
|
if is_placeholder(rel):
|
|
@@ -182,6 +194,8 @@ def _index_listing(root: str, focus: str | None = None, limit: int = UNIT_MAX_FI
|
|
|
182
194
|
continue
|
|
183
195
|
if listed is not None and rel not in listed:
|
|
184
196
|
continue
|
|
197
|
+
if rules and gitignored(rules, rel, False):
|
|
198
|
+
continue
|
|
185
199
|
if kind_of(rel, root=root) is None:
|
|
186
200
|
continue
|
|
187
201
|
try:
|
|
@@ -990,9 +1004,8 @@ class IntelService:
|
|
|
990
1004
|
|
|
991
1005
|
# ── code graph everywhere (plan 3 Tasks 7, 9): the local workspace in the vault's one graph ─────────
|
|
992
1006
|
|
|
993
|
-
# what makes a folder a project for the code graph: version control or
|
|
994
|
-
#
|
|
995
|
-
_MANIFESTS = tuple(indexer.CONFIG_NAMES) + ("setup.py", "requirements.txt", "tsconfig.json")
|
|
1007
|
+
# what makes a folder a project for the code graph: version control or any ecosystem's manifest at its root -- the
|
|
1008
|
+
# sdk's one rule (``indexer.is_project_root``: Cargo.toml, go.mod, pom.xml, *.csproj, composer.json, Gemfile, ...)
|
|
996
1009
|
|
|
997
1010
|
|
|
998
1011
|
SUB_REPOS_MAX = 40 # a folder of projects: at most this many of its repositories are synced
|
|
@@ -1025,7 +1038,7 @@ def is_repository(root: str) -> bool:
|
|
|
1025
1038
|
names = set(os.listdir(root))
|
|
1026
1039
|
except OSError:
|
|
1027
1040
|
return False
|
|
1028
|
-
return
|
|
1041
|
+
return indexer.is_project_root(names)
|
|
1029
1042
|
|
|
1030
1043
|
|
|
1031
1044
|
_MACHINE_ID_FILE = os.path.join(os.path.expanduser("~"), ".local", "share", "webbee", "machine-id")
|
|
@@ -1002,7 +1002,11 @@ class LocalToolExecutor:
|
|
|
1002
1002
|
|
|
1003
1003
|
def _cpc_local(self, verb: str, a: dict) -> dict:
|
|
1004
1004
|
if self.indexer is None:
|
|
1005
|
-
|
|
1005
|
+
from webbee import boot
|
|
1006
|
+
why = boot.INTEL_DOWN_REASON or "the code index is not running in this session"
|
|
1007
|
+
return {"ok": False, "content": f"intel not available: {why}. It ships with webbee (no extra to install); "
|
|
1008
|
+
"restart webbee in the repository, and if it repeats, see "
|
|
1009
|
+
"~/.cache/webbee/tui-stderr.log"}
|
|
1006
1010
|
from webbee.intel import query
|
|
1007
1011
|
if verb == "repo_profile":
|
|
1008
1012
|
return query.repo_profile(self.indexer)
|
|
@@ -1366,7 +1370,7 @@ async def _no_repo_at(sftp, ci, root: str) -> str:
|
|
|
1366
1370
|
names = {e.filename for e in await sftp.readdir(repo_root)}
|
|
1367
1371
|
except Exception:
|
|
1368
1372
|
names = set()
|
|
1369
|
-
if not
|
|
1373
|
+
if not ci.is_project_root(names):
|
|
1370
1374
|
return f"{root} is in no repository (no .git or project manifest at it or above)"
|
|
1371
1375
|
return ""
|
|
1372
1376
|
|
|
@@ -95,3 +95,24 @@ def test_a_secret_written_later_is_never_read_and_the_profile_names_no_document(
|
|
|
95
95
|
assert "credentials.json" not in svc.index.files and not any("credentials.json" in o for o in opened)
|
|
96
96
|
prof = svc.repo_profile()
|
|
97
97
|
assert not any("Agreement" in s for s in prof["top_symbols"]) and prof["other_kinds"]["document"] >= 2
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_gitignore_applies_without_git(tmp_path, monkeypatch):
|
|
102
|
+
"""06.11 report: in a folder without git (or on a machine without the git binary) no .gitignore applied."""
|
|
103
|
+
from webbee.intel import service
|
|
104
|
+
(tmp_path / ".gitignore").write_text("dumps/\n*.png\n")
|
|
105
|
+
(tmp_path / "dumps").mkdir()
|
|
106
|
+
(tmp_path / "dumps" / "big.sql").write_text("x")
|
|
107
|
+
(tmp_path / "shot.png").write_bytes(b"\x89PNG")
|
|
108
|
+
(tmp_path / "app.py").write_text("x = 1\n")
|
|
109
|
+
(tmp_path / "sub").mkdir()
|
|
110
|
+
(tmp_path / "sub" / ".gitignore").write_text("local.py\n")
|
|
111
|
+
(tmp_path / "sub" / "local.py").write_text("y = 1\n")
|
|
112
|
+
(tmp_path / "sub" / "keep.py").write_text("z = 1\n")
|
|
113
|
+
rels, _stats, _skipped = service._index_listing(str(tmp_path))
|
|
114
|
+
assert "app.py" in rels and "sub/keep.py" in rels
|
|
115
|
+
assert not {"dumps/big.sql", "shot.png", "sub/local.py"} & set(rels)
|
|
116
|
+
# the same with git present but failing (no binary): the rules still apply
|
|
117
|
+
monkeypatch.setattr(service, "_git_listed", lambda root: None)
|
|
118
|
+
assert "sub/local.py" not in service._index_listing(str(tmp_path))[0]
|
|
@@ -5,7 +5,7 @@ from webbee.intel import query
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def _svc(tmp_path):
|
|
8
|
-
(tmp_path / "a.py").write_text("
|
|
8
|
+
(tmp_path / "a.py").write_text("from b import beta\n\n\ndef alpha():\n return beta()\n")
|
|
9
9
|
(tmp_path / "b.py").write_text("def beta():\n return 1\n")
|
|
10
10
|
s = IntelService(str(tmp_path), "rk", cache_dir=str(tmp_path / "c")); s.build(); return s
|
|
11
11
|
|
|
@@ -83,3 +83,32 @@ def test_search_code_kind_filter_does_not_nullify_vector_arm(tmp_path, monkeypat
|
|
|
83
83
|
monkeypatch.setattr(query, "_lexical_search", lambda *a, **k: [])
|
|
84
84
|
r = query.search_code(svc, "beta", kind="function")
|
|
85
85
|
assert r["ok"] and any(i["title"] == "beta" for i in r["data"]["items"])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# 06.11 report: a function nobody calls answered hundreds of files (links by bare name, depth through every def of a
|
|
89
|
+
# hit file). The local answer now follows the vault's rules (imperal_sdk.code_links).
|
|
90
|
+
|
|
91
|
+
def _svc2(tmp_path):
|
|
92
|
+
(tmp_path / "core.py").write_text("def used():\n return 1\n\n\ndef lonely():\n return 2\n")
|
|
93
|
+
(tmp_path / "user.py").write_text("from core import used\n\n\ndef run():\n return used()\n")
|
|
94
|
+
(tmp_path / "other.py").write_text("def go(obj):\n return obj.lonely()\n\n\ndef used_twice():\n return 3\n")
|
|
95
|
+
(tmp_path / "tests").mkdir()
|
|
96
|
+
(tmp_path / "tests" / "test_core.py").write_text("from core import used\n\n\ndef test_used():\n assert used()\n")
|
|
97
|
+
s = IntelService(str(tmp_path), "rk2", cache_dir=str(tmp_path / "c")); s.build(); return s
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_impact_nobody_calls_is_empty_and_by_name_is_apart(tmp_path):
|
|
101
|
+
r = query.impact_of_change(_svc2(tmp_path), ["lonely"])
|
|
102
|
+
assert r["data"]["items"] == [] and r["resolved"] == ["core.py::lonely"]
|
|
103
|
+
assert [b["path"] for b in r["by_name"]] == ["other.py"]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_impact_resolved_users_code_before_tests(tmp_path):
|
|
107
|
+
r = query.impact_of_change(_svc2(tmp_path), ["used"])
|
|
108
|
+
assert [i["id"] for i in r["data"]["items"]] == ["user.py", "tests/test_core.py"]
|
|
109
|
+
assert r["data"]["items"][0]["user"] == "run" and r["data"]["items"][0]["line"] == 5
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_impact_unknown_symbol_says_so(tmp_path):
|
|
113
|
+
r = query.impact_of_change(_svc2(tmp_path), ["nope"])
|
|
114
|
+
assert r["data"]["items"] == [] and "not a definition" in r["note"]
|
|
@@ -178,6 +178,21 @@ def test_cpc_shim_degrades_without_indexer(tmp_path):
|
|
|
178
178
|
ex = LocalToolExecutor(str(tmp_path)) # indexer=None
|
|
179
179
|
out = ex.run("graph_slice", {"symbols": ["x"]})
|
|
180
180
|
assert out["ok"] is False and "intel not available" in out["content"]
|
|
181
|
+
assert "install webbee[intel]" not in out["content"] # intel ships in the base install since 0.7.0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_cpc_shim_says_why_the_index_is_down(tmp_path, monkeypatch):
|
|
185
|
+
import asyncio
|
|
186
|
+
from webbee import boot
|
|
187
|
+
from webbee.tools import LocalToolExecutor
|
|
188
|
+
|
|
189
|
+
def broken_factory(cfg, ws):
|
|
190
|
+
raise PermissionError("cannot write the index cache")
|
|
191
|
+
monkeypatch.setattr(boot, "INTEL_DOWN_REASON", "")
|
|
192
|
+
assert asyncio.run(boot.start_intel(object(), str(tmp_path), broken_factory)) == (None, None)
|
|
193
|
+
out = LocalToolExecutor(str(tmp_path)).run("impact_of_change", {"symbols": ["x"]})
|
|
194
|
+
assert out["ok"] is False
|
|
195
|
+
assert "PermissionError: cannot write the index cache" in out["content"]
|
|
181
196
|
|
|
182
197
|
|
|
183
198
|
def test_cpc_graph_slice_coerces_stringified_symbols(tmp_path):
|
|
@@ -187,7 +202,7 @@ def test_cpc_graph_slice_coerces_stringified_symbols(tmp_path):
|
|
|
187
202
|
# false negative the brain reads as "no callers".
|
|
188
203
|
pytest.importorskip("tree_sitter")
|
|
189
204
|
from webbee.intel.service import IntelService
|
|
190
|
-
(tmp_path / "a.py").write_text("
|
|
205
|
+
(tmp_path / "a.py").write_text("from b import beta\n\n\ndef alpha():\n return beta()\n") # a real import: links are resolved
|
|
191
206
|
(tmp_path / "b.py").write_text("def beta():\n return 1\n")
|
|
192
207
|
svc = IntelService(str(tmp_path), "rk", cache_dir=str(tmp_path / "c"))
|
|
193
208
|
svc.build()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.7.0"
|
|
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
|