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.
Files changed (159) hide show
  1. {webbee-0.7.0 → webbee-0.7.2}/CHANGELOG.md +24 -0
  2. {webbee-0.7.0 → webbee-0.7.2}/PKG-INFO +3 -3
  3. {webbee-0.7.0 → webbee-0.7.2}/pyproject.toml +3 -3
  4. webbee-0.7.2/src/webbee/__init__.py +1 -0
  5. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/boot.py +11 -1
  6. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/query.py +90 -3
  7. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/service.py +18 -5
  8. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/tools.py +6 -2
  9. {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_any_folder.py +21 -0
  10. {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_query.py +30 -1
  11. {webbee-0.7.0 → webbee-0.7.2}/tests/test_tools.py +16 -1
  12. webbee-0.7.0/src/webbee/__init__.py +0 -1
  13. {webbee-0.7.0 → webbee-0.7.2}/.github/workflows/publish.yml +0 -0
  14. {webbee-0.7.0 → webbee-0.7.2}/.gitignore +0 -0
  15. {webbee-0.7.0 → webbee-0.7.2}/LICENSE +0 -0
  16. {webbee-0.7.0 → webbee-0.7.2}/README.md +0 -0
  17. {webbee-0.7.0 → webbee-0.7.2}/install.sh +0 -0
  18. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/__main__.py +0 -0
  19. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/_launch.py +0 -0
  20. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/account.py +0 -0
  21. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/active_sessions.py +0 -0
  22. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/autoupdate.py +0 -0
  23. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/banner_art.py +0 -0
  24. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/browser.py +0 -0
  25. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/checkpoints.py +0 -0
  26. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/cli.py +0 -0
  27. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/clipboard.py +0 -0
  28. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/clipboard_read.py +0 -0
  29. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/clipboard_session.py +0 -0
  30. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/coding_context.py +0 -0
  31. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/commands.py +0 -0
  32. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/config.py +0 -0
  33. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/consent.py +0 -0
  34. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/details.py +0 -0
  35. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/events.py +0 -0
  36. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/frames.py +0 -0
  37. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/home.py +0 -0
  38. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/home_view.py +0 -0
  39. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/http.py +0 -0
  40. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/instance_lock.py +0 -0
  41. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/__init__.py +0 -0
  42. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/graph.py +0 -0
  43. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/outline.py +0 -0
  44. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/store.py +0 -0
  45. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/tree.py +0 -0
  46. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/vectors.py +0 -0
  47. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/intel/watch.py +0 -0
  48. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/media.py +0 -0
  49. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/modals.py +0 -0
  50. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/mode_store.py +0 -0
  51. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/native_files.py +0 -0
  52. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/native_read.py +0 -0
  53. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/newtab_mode.py +0 -0
  54. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/output_pane.py +0 -0
  55. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/pricing_constants.py +0 -0
  56. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/queue_panel.py +0 -0
  57. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/reflow.py +0 -0
  58. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/remote.py +0 -0
  59. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/render.py +0 -0
  60. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/repl.py +0 -0
  61. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/repo.py +0 -0
  62. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/selection.py +0 -0
  63. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/session.py +0 -0
  64. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/sessions.py +0 -0
  65. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/sizing.py +0 -0
  66. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/slots.py +0 -0
  67. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/steer.py +0 -0
  68. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/stream.py +0 -0
  69. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/tab_store.py +0 -0
  70. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/tabs.py +0 -0
  71. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/thread.py +0 -0
  72. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/tier_store.py +0 -0
  73. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/todo_panel.py +0 -0
  74. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/tokens.py +0 -0
  75. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/tui.py +0 -0
  76. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/update.py +0 -0
  77. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/urlopen.py +0 -0
  78. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/wallet.py +0 -0
  79. {webbee-0.7.0 → webbee-0.7.2}/src/webbee/worktrees.py +0 -0
  80. {webbee-0.7.0 → webbee-0.7.2}/tests/__init__.py +0 -0
  81. {webbee-0.7.0 → webbee-0.7.2}/tests/conftest.py +0 -0
  82. {webbee-0.7.0 → webbee-0.7.2}/tests/test_account.py +0 -0
  83. {webbee-0.7.0 → webbee-0.7.2}/tests/test_active_sessions.py +0 -0
  84. {webbee-0.7.0 → webbee-0.7.2}/tests/test_audit_2026_07_25_findings.py +0 -0
  85. {webbee-0.7.0 → webbee-0.7.2}/tests/test_autoupdate.py +0 -0
  86. {webbee-0.7.0 → webbee-0.7.2}/tests/test_checkpoints.py +0 -0
  87. {webbee-0.7.0 → webbee-0.7.2}/tests/test_checkpoints_nested_1030.py +0 -0
  88. {webbee-0.7.0 → webbee-0.7.2}/tests/test_cli.py +0 -0
  89. {webbee-0.7.0 → webbee-0.7.2}/tests/test_clipboard.py +0 -0
  90. {webbee-0.7.0 → webbee-0.7.2}/tests/test_clipboard_read.py +0 -0
  91. {webbee-0.7.0 → webbee-0.7.2}/tests/test_code_map_fresh.py +0 -0
  92. {webbee-0.7.0 → webbee-0.7.2}/tests/test_coding_context.py +0 -0
  93. {webbee-0.7.0 → webbee-0.7.2}/tests/test_commands.py +0 -0
  94. {webbee-0.7.0 → webbee-0.7.2}/tests/test_config.py +0 -0
  95. {webbee-0.7.0 → webbee-0.7.2}/tests/test_cpc_contract_stable.py +0 -0
  96. {webbee-0.7.0 → webbee-0.7.2}/tests/test_cpc_outline.py +0 -0
  97. {webbee-0.7.0 → webbee-0.7.2}/tests/test_cross_loop_token.py +0 -0
  98. {webbee-0.7.0 → webbee-0.7.2}/tests/test_details.py +0 -0
  99. {webbee-0.7.0 → webbee-0.7.2}/tests/test_events.py +0 -0
  100. {webbee-0.7.0 → webbee-0.7.2}/tests/test_freeze_fix.py +0 -0
  101. {webbee-0.7.0 → webbee-0.7.2}/tests/test_home.py +0 -0
  102. {webbee-0.7.0 → webbee-0.7.2}/tests/test_home_view.py +0 -0
  103. {webbee-0.7.0 → webbee-0.7.2}/tests/test_home_view_mouse.py +0 -0
  104. {webbee-0.7.0 → webbee-0.7.2}/tests/test_inspect_page.py +0 -0
  105. {webbee-0.7.0 → webbee-0.7.2}/tests/test_instance_lock.py +0 -0
  106. {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_cache_and_sync_worker.py +0 -0
  107. {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_checksum_tree.py +0 -0
  108. {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_chunker.py +0 -0
  109. {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_embed.py +0 -0
  110. {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_graph.py +0 -0
  111. {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_no_numpy.py +0 -0
  112. {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_service.py +0 -0
  113. {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_store.py +0 -0
  114. {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_vectors.py +0 -0
  115. {webbee-0.7.0 → webbee-0.7.2}/tests/test_intel_watch.py +0 -0
  116. {webbee-0.7.0 → webbee-0.7.2}/tests/test_launch.py +0 -0
  117. {webbee-0.7.0 → webbee-0.7.2}/tests/test_marathon.py +0 -0
  118. {webbee-0.7.0 → webbee-0.7.2}/tests/test_modals_e2e.py +0 -0
  119. {webbee-0.7.0 → webbee-0.7.2}/tests/test_mode_and_remote_0_6_8.py +0 -0
  120. {webbee-0.7.0 → webbee-0.7.2}/tests/test_mode_store.py +0 -0
  121. {webbee-0.7.0 → webbee-0.7.2}/tests/test_native_files.py +0 -0
  122. {webbee-0.7.0 → webbee-0.7.2}/tests/test_newtab_mode.py +0 -0
  123. {webbee-0.7.0 → webbee-0.7.2}/tests/test_packaging.py +0 -0
  124. {webbee-0.7.0 → webbee-0.7.2}/tests/test_probe_cost.py +0 -0
  125. {webbee-0.7.0 → webbee-0.7.2}/tests/test_queue_manage_0337.py +0 -0
  126. {webbee-0.7.0 → webbee-0.7.2}/tests/test_reflow.py +0 -0
  127. {webbee-0.7.0 → webbee-0.7.2}/tests/test_remote_code_reads.py +0 -0
  128. {webbee-0.7.0 → webbee-0.7.2}/tests/test_render.py +0 -0
  129. {webbee-0.7.0 → webbee-0.7.2}/tests/test_repl.py +0 -0
  130. {webbee-0.7.0 → webbee-0.7.2}/tests/test_repo.py +0 -0
  131. {webbee-0.7.0 → webbee-0.7.2}/tests/test_resilient_http.py +0 -0
  132. {webbee-0.7.0 → webbee-0.7.2}/tests/test_review_fixes_1030.py +0 -0
  133. {webbee-0.7.0 → webbee-0.7.2}/tests/test_session.py +0 -0
  134. {webbee-0.7.0 → webbee-0.7.2}/tests/test_session_review_1030.py +0 -0
  135. {webbee-0.7.0 → webbee-0.7.2}/tests/test_sessions.py +0 -0
  136. {webbee-0.7.0 → webbee-0.7.2}/tests/test_sizing.py +0 -0
  137. {webbee-0.7.0 → webbee-0.7.2}/tests/test_slots.py +0 -0
  138. {webbee-0.7.0 → webbee-0.7.2}/tests/test_steer.py +0 -0
  139. {webbee-0.7.0 → webbee-0.7.2}/tests/test_stream.py +0 -0
  140. {webbee-0.7.0 → webbee-0.7.2}/tests/test_tab_bar_stability.py +0 -0
  141. {webbee-0.7.0 → webbee-0.7.2}/tests/test_tab_restore_0337.py +0 -0
  142. {webbee-0.7.0 → webbee-0.7.2}/tests/test_tabs.py +0 -0
  143. {webbee-0.7.0 → webbee-0.7.2}/tests/test_terminal_ux_0336.py +0 -0
  144. {webbee-0.7.0 → webbee-0.7.2}/tests/test_thread.py +0 -0
  145. {webbee-0.7.0 → webbee-0.7.2}/tests/test_tier_invalidation.py +0 -0
  146. {webbee-0.7.0 → webbee-0.7.2}/tests/test_tier_store.py +0 -0
  147. {webbee-0.7.0 → webbee-0.7.2}/tests/test_todo_panel.py +0 -0
  148. {webbee-0.7.0 → webbee-0.7.2}/tests/test_tokens.py +0 -0
  149. {webbee-0.7.0 → webbee-0.7.2}/tests/test_tools_eol_cancel_1030.py +0 -0
  150. {webbee-0.7.0 → webbee-0.7.2}/tests/test_tools_vision_websearch.py +0 -0
  151. {webbee-0.7.0 → webbee-0.7.2}/tests/test_tui.py +0 -0
  152. {webbee-0.7.0 → webbee-0.7.2}/tests/test_tui_hardening.py +0 -0
  153. {webbee-0.7.0 → webbee-0.7.2}/tests/test_update.py +0 -0
  154. {webbee-0.7.0 → webbee-0.7.2}/tests/test_update_badge_0340.py +0 -0
  155. {webbee-0.7.0 → webbee-0.7.2}/tests/test_urlopen.py +0 -0
  156. {webbee-0.7.0 → webbee-0.7.2}/tests/test_version.py +0 -0
  157. {webbee-0.7.0 → webbee-0.7.2}/tests/test_version_badge_corner.py +0 -0
  158. {webbee-0.7.0 → webbee-0.7.2}/tests/test_wallet.py +0 -0
  159. {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.0
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.0; python_version >= '3.10'
35
- Requires-Dist: imperal-sdk[universal-index]>=6.4.1; python_version >= '3.10'
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.0"
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.0; python_version >= "3.10"', # raised to the newest at each release
17
- 'imperal-sdk[universal-index]>=6.4.1; python_version >= "3.10"',
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
- files = svc.graph.dependents_of(list(symbols or []), depth=3)
195
- items = [{"id": f, "title": f, "kind": "file"} for f in sorted(files)]
196
- return _env(items)
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 a manifest of an indexable language at its root
994
- # (the sdk's config names; Python and JS/TS are what the index reads)
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 ".git" in names or any(m in names for m in _MANIFESTS)
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
- return {"ok": False, "content": "intel not available; install webbee[intel]"}
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 names & (set(getattr(ci, "_REPO_MARKERS", (".git",))) | set(getattr(ci, "_MANIFESTS", ()))):
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("def alpha():\n return beta()\n")
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("def alpha():\n return beta()\n")
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