webbee 0.7.2__tar.gz → 0.7.3__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.2 → webbee-0.7.3}/CHANGELOG.md +19 -0
- {webbee-0.7.2 → webbee-0.7.3}/PKG-INFO +3 -3
- {webbee-0.7.2 → webbee-0.7.3}/pyproject.toml +3 -3
- webbee-0.7.3/src/webbee/__init__.py +1 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/intel/query.py +37 -7
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/intel/service.py +113 -26
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/tools.py +5 -2
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_intel_cache_and_sync_worker.py +1 -1
- webbee-0.7.3/tests/test_intel_folder_1106.py +109 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_tools.py +8 -1
- webbee-0.7.2/src/webbee/__init__.py +0 -1
- {webbee-0.7.2 → webbee-0.7.3}/.github/workflows/publish.yml +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/.gitignore +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/LICENSE +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/README.md +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/install.sh +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/__main__.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/_launch.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/account.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/active_sessions.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/autoupdate.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/banner_art.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/boot.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/browser.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/checkpoints.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/cli.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/clipboard.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/clipboard_read.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/clipboard_session.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/coding_context.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/commands.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/config.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/consent.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/details.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/events.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/frames.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/home.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/home_view.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/http.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/instance_lock.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/intel/graph.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/intel/outline.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/intel/store.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/intel/tree.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/intel/watch.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/media.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/modals.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/mode_store.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/native_files.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/native_read.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/newtab_mode.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/output_pane.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/pricing_constants.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/queue_panel.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/reflow.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/remote.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/render.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/repl.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/repo.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/selection.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/session.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/sessions.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/sizing.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/slots.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/steer.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/stream.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/tab_store.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/tabs.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/thread.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/tier_store.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/todo_panel.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/tokens.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/tui.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/update.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/urlopen.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/wallet.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/src/webbee/worktrees.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/__init__.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/conftest.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_account.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_active_sessions.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_audit_2026_07_25_findings.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_autoupdate.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_checkpoints.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_checkpoints_nested_1030.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_cli.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_clipboard.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_clipboard_read.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_code_map_fresh.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_coding_context.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_commands.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_config.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_cpc_outline.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_cross_loop_token.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_details.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_events.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_freeze_fix.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_home.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_home_view.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_home_view_mouse.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_inspect_page.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_instance_lock.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_intel_any_folder.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_intel_checksum_tree.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_intel_chunker.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_intel_embed.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_intel_graph.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_intel_query.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_intel_service.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_intel_store.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_intel_vectors.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_intel_watch.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_launch.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_marathon.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_modals_e2e.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_mode_and_remote_0_6_8.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_mode_store.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_native_files.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_newtab_mode.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_packaging.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_probe_cost.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_queue_manage_0337.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_reflow.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_remote_code_reads.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_render.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_repl.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_repo.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_resilient_http.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_review_fixes_1030.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_session.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_session_review_1030.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_sessions.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_sizing.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_slots.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_steer.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_stream.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_tab_bar_stability.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_tab_restore_0337.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_tabs.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_terminal_ux_0336.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_thread.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_tier_invalidation.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_tier_store.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_todo_panel.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_tokens.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_tools_eol_cancel_1030.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_tools_vision_websearch.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_tui.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_tui_hardening.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_update.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_update_badge_0340.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_urlopen.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_version.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_version_badge_corner.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_wallet.py +0 -0
- {webbee-0.7.2 → webbee-0.7.3}/tests/test_worktrees.py +0 -0
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.3
|
|
4
|
+
|
|
5
|
+
* **A folder of repositories is a folder** (second 06.11 report).
|
|
6
|
+
* Repositories nested deeper (`core/<repo>`, `modules/<repo>`) are found, up to four levels.
|
|
7
|
+
* Each repository syncs on its own, even when the folder's top holds loose scripts or a manifest. Before, the
|
|
8
|
+
whole folder was one 670-file project.
|
|
9
|
+
* Imports are resolved inside each repository first; a package another repository provides is linked to it.
|
|
10
|
+
* **The cloud graph answers from the first minute.**
|
|
11
|
+
* While the session's first sync runs, the vault answers with the keys it confirmed last time, and the answer
|
|
12
|
+
says so.
|
|
13
|
+
* "pending" ends with that first run. Before, every tool call queued a sync and the kernel always met one running.
|
|
14
|
+
* **Facts, not names.**
|
|
15
|
+
* The `read_file` header lists only the files whose references resolve to the file (it said "used by
|
|
16
|
+
check-paths-agree.py, +50 more").
|
|
17
|
+
* `graph_slice` callers are the real calls, with line and caller.
|
|
18
|
+
* `impact_of_change` says in one sentence what it found, including name-only matches as unproven.
|
|
19
|
+
* Python members resolve through typed locals, `self` attributes and return annotations.
|
|
20
|
+
* Needs imperal-mcp 0.9.3 and imperal-sdk 6.5.1.
|
|
21
|
+
|
|
3
22
|
## 0.7.2
|
|
4
23
|
|
|
5
24
|
* **The code graph answers correctly** (06.11 report):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: webbee
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.3
|
|
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.5.
|
|
34
|
+
Requires-Dist: imperal-mcp>=0.9.3; python_version >= '3.10'
|
|
35
|
+
Requires-Dist: imperal-sdk[universal-index]>=6.5.1; 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.3"
|
|
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.5.
|
|
16
|
+
'imperal-mcp>=0.9.3; python_version >= "3.10"', # raised to the newest at each release
|
|
17
|
+
'imperal-sdk[universal-index]>=6.5.1; 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.3"
|
|
@@ -14,22 +14,42 @@ def _sym_item(s) -> dict:
|
|
|
14
14
|
|
|
15
15
|
def repo_profile(svc) -> dict:
|
|
16
16
|
p = svc.repo_profile()
|
|
17
|
-
|
|
17
|
+
repos = p.get("repos") or []
|
|
18
|
+
title = (f"folder ({p['file_count']} files, {len(repos)} repositories: {', '.join(repos[:12])}"
|
|
19
|
+
+ (" ..." if len(repos) > 12 else "") + ")") if repos else f"repo ({p['file_count']} files)"
|
|
20
|
+
item = {"id": p["repo_key"], "title": title, "kind": "folder" if repos else "repo", **p}
|
|
18
21
|
return _env([item])
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def graph_slice(svc, symbols, depth: int = 1) -> dict:
|
|
25
|
+
"""Each definition a symbol names, with its RESOLVED callers (file, line, the calling definition) and callees
|
|
26
|
+
(what its body's references resolve to) -- the vault's rules (``imperal_sdk.code_links``). Before, the callers
|
|
27
|
+
were every definition of every file where the name appeared (report 06.11 #2)."""
|
|
22
28
|
if svc is None or svc.graph is None:
|
|
23
29
|
return {"ok": False, "content": "index not ready"}
|
|
24
30
|
if isinstance(symbols, str):
|
|
25
31
|
symbols = [symbols]
|
|
26
|
-
|
|
32
|
+
lk = linker(svc)
|
|
33
|
+
links = lk.links()
|
|
34
|
+
items = []
|
|
27
35
|
for name in (symbols or []):
|
|
28
|
-
for
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
for path, qual in lk.symbol_targets(str(name)):
|
|
37
|
+
if not qual:
|
|
38
|
+
continue
|
|
39
|
+
d = lk.defs(path).get(qual)
|
|
40
|
+
it = {"id": f"{path}::{qual}", "title": qual, "kind": getattr(d, "kind", ""),
|
|
41
|
+
"subtitle": f"{path}:{getattr(d, 'start_line', 0)}", "signature": getattr(d, "signature", ""),
|
|
42
|
+
"path": path, "start_line": getattr(d, "start_line", 0), "end_line": getattr(d, "end_line", 0)}
|
|
43
|
+
callers = [{"path": src, "user": user or "(module)", "line": line, "kind": kind}
|
|
44
|
+
for src, user, kind, line, tp, tq, _o in links
|
|
45
|
+
if tp == path and (tq == qual or tq.startswith(qual + ".")) and not (src == path and user == qual)]
|
|
46
|
+
it["callers"] = callers[:20]
|
|
47
|
+
it["callers_total"] = len(callers)
|
|
48
|
+
it["callees"] = sorted({f"{tp}::{tq}" if tq else tp for src, user, _k, _l, tp, tq, _o in links
|
|
49
|
+
if src == path and (user == qual or user.startswith(qual + "."))})[:20]
|
|
32
50
|
items.append(it)
|
|
51
|
+
if not items:
|
|
52
|
+
return {**_env([]), "note": f"{', '.join(map(str, symbols or []))}: not a definition in this index"}
|
|
33
53
|
return _env(items)
|
|
34
54
|
|
|
35
55
|
|
|
@@ -247,7 +267,14 @@ def linker(svc):
|
|
|
247
267
|
except Exception:
|
|
248
268
|
continue
|
|
249
269
|
configs.append({"path": rel.replace("\\", "/"), "kind": kind, "data": data})
|
|
250
|
-
|
|
270
|
+
roots = []
|
|
271
|
+
if root:
|
|
272
|
+
try:
|
|
273
|
+
from webbee.intel.service import is_repository, sub_repositories
|
|
274
|
+
roots = [] if is_repository(root) else sub_repositories(root)
|
|
275
|
+
except Exception:
|
|
276
|
+
roots = []
|
|
277
|
+
lk = Linker(facts, configs, roots)
|
|
251
278
|
svc._linker, svc._linker_graph = lk, g
|
|
252
279
|
return lk
|
|
253
280
|
|
|
@@ -277,6 +304,9 @@ def impact_of_change(svc, symbols) -> dict:
|
|
|
277
304
|
if extra:
|
|
278
305
|
out["by_name_count"] = int(out.get("by_name_count") or 0) + len(extra)
|
|
279
306
|
out["by_name"] = sorted(out["by_name"] + extra, key=lambda b: (b["group"] == "tests", b["path"]))[:BY_NAME_SHOWN]
|
|
307
|
+
from imperal_sdk.code_links import impact_summary
|
|
308
|
+
out["summary"] = impact_summary(syms, out["resolved"], len(out["data"]["items"]), int(out.get("by_name_count") or 0),
|
|
309
|
+
len(out.get("same_file") or []))
|
|
280
310
|
if not out["resolved"]:
|
|
281
311
|
out["note"] = f"{', '.join(syms)}: not a definition in this index"
|
|
282
312
|
out["link"] = "resolved locally (the vault's rules); by_name lists name-only matches apart"
|
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import hashlib
|
|
9
9
|
import logging
|
|
10
|
+
import json
|
|
10
11
|
import os
|
|
11
12
|
import subprocess
|
|
12
13
|
import sys
|
|
@@ -754,9 +755,10 @@ class IntelService:
|
|
|
754
755
|
if rels is not None: # a change: only the repositories it touched, with their own paths
|
|
755
756
|
by_repo: dict = {}
|
|
756
757
|
for rel in rels:
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
758
|
+
rel = str(rel).replace(os.sep, "/")
|
|
759
|
+
head = repo_of(rel, subs)
|
|
760
|
+
if head and rel != head:
|
|
761
|
+
by_repo.setdefault(head, []).append(rel[len(head) + 1:])
|
|
760
762
|
todo = {name: by_repo[name] for name in by_repo}
|
|
761
763
|
else:
|
|
762
764
|
todo = {name: None for name in subs}
|
|
@@ -766,6 +768,8 @@ class IntelService:
|
|
|
766
768
|
if out.get("repo_key"):
|
|
767
769
|
repos[name] = str(out["repo_key"])
|
|
768
770
|
out = {"repo_key": "", "repos": repos}
|
|
771
|
+
if repos and rels is None:
|
|
772
|
+
remember_keys(self.root, out)
|
|
769
773
|
if repos:
|
|
770
774
|
self.last_sync = out
|
|
771
775
|
self.last_sync_ok_at = time.time()
|
|
@@ -815,6 +819,8 @@ class IntelService:
|
|
|
815
819
|
return out
|
|
816
820
|
measured = co_change_pairs(root) if rels is None and history else None
|
|
817
821
|
out = asyncio.run(vault_sync(factory(), meta, listing, read, measured))
|
|
822
|
+
if out.get("repo_key") and rels is None and os.path.realpath(root) == os.path.realpath(self.root):
|
|
823
|
+
remember_keys(root, out)
|
|
818
824
|
if sig and out.get("repo_key"):
|
|
819
825
|
self._synced_sigs[root] = (sig, str(out["repo_key"]), time.time())
|
|
820
826
|
elif rels is not None:
|
|
@@ -844,10 +850,16 @@ class IntelService:
|
|
|
844
850
|
extract + embed, minutes) it answers at once ``{pending}`` -- a state, not a failure. ``wait_s``: tests only."""
|
|
845
851
|
if self.vault_client_factory is None:
|
|
846
852
|
return {"ok": False, "content": "this session cannot reach the cloud"}
|
|
847
|
-
if
|
|
848
|
-
# the
|
|
849
|
-
|
|
850
|
-
|
|
853
|
+
if self._sync_done == 0 and self._sync_busy():
|
|
854
|
+
# the session's FIRST sync is still running (a whole repo: extract + embed): answered at once, as a state
|
|
855
|
+
# -- with the keys the vault confirmed for this folder last time, so the cloud graph answers meanwhile
|
|
856
|
+
# (marked by its age). Only the first run: after it, each answer waits for its own sync (report 06.11 #2:
|
|
857
|
+
# every tool call queued a sync and every kernel ask met one running -- "pending" for a whole session)
|
|
858
|
+
known = remembered_keys(self.root)
|
|
859
|
+
return {"pending": "the first code sync of this workspace is still running -- "
|
|
860
|
+
+ ("the cloud graph answers from the last confirmed sync"
|
|
861
|
+
if known else "the local index answers until it ends"),
|
|
862
|
+
**({"known": known} if known else {})}
|
|
851
863
|
ticket = self._sync_request(None, history=False)
|
|
852
864
|
with self._sync_cv:
|
|
853
865
|
done = self._sync_cv.wait_for(lambda: self._sync_done >= ticket, timeout=wait_s)
|
|
@@ -1009,38 +1021,113 @@ class IntelService:
|
|
|
1009
1021
|
|
|
1010
1022
|
|
|
1011
1023
|
SUB_REPOS_MAX = 40 # a folder of projects: at most this many of its repositories are synced
|
|
1024
|
+
REPO_DEPTH = 4 # repositories are looked for this deep (~/Projects/<org>/<repo>, a folder's core/<repo>)
|
|
1025
|
+
_REPOS_TTL_S = 30.0 # a folder's repositories, remembered this long (is_repository asks on every tool call)
|
|
1026
|
+
_REPOS_SEEN: dict = {}
|
|
1027
|
+
# a home folder's system trees: never walked for repositories (macOS Library / Applications, Volumes ...)
|
|
1028
|
+
_TOP_NEVER = frozenset({"Library", "Applications", "System", "Volumes", "private", "cores", "dev"})
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def find_repositories(root: str, depth: int = REPO_DEPTH) -> list:
|
|
1032
|
+
"""The repositories inside ``root`` (folders holding ``.git``: a directory, or a file in a worktree), as
|
|
1033
|
+
root-relative posix paths, at most ``depth`` levels down -- never under a hidden, skipped or linked folder, never
|
|
1034
|
+
inside another repository (report 06.11 #2: core/wpanel-sdk, modules/wpanel-tickets were one level too deep and
|
|
1035
|
+
the whole folder became one project). The ONE rule for Webbee and imperal-mcp."""
|
|
1036
|
+
key = (os.path.realpath(root), int(depth))
|
|
1037
|
+
hit = _REPOS_SEEN.get(key)
|
|
1038
|
+
if hit and time.monotonic() - hit[0] < _REPOS_TTL_S:
|
|
1039
|
+
return list(hit[1])
|
|
1040
|
+
out: list = []
|
|
1041
|
+
frontier = [""]
|
|
1042
|
+
for _ in range(max(1, int(depth))):
|
|
1043
|
+
nxt: list = []
|
|
1044
|
+
for rel in frontier:
|
|
1045
|
+
here = os.path.join(root, rel) if rel else root
|
|
1046
|
+
try:
|
|
1047
|
+
names = sorted(os.listdir(here))
|
|
1048
|
+
except OSError:
|
|
1049
|
+
continue
|
|
1050
|
+
for name in names:
|
|
1051
|
+
full = os.path.join(here, name)
|
|
1052
|
+
if (not rel and name in _TOP_NEVER) or os.path.islink(full) or not os.path.isdir(full) \
|
|
1053
|
+
or skipped_dir(here, name):
|
|
1054
|
+
continue
|
|
1055
|
+
child = f"{rel}/{name}" if rel else name
|
|
1056
|
+
(out if os.path.exists(os.path.join(full, ".git")) else nxt).append(child)
|
|
1057
|
+
frontier = nxt
|
|
1058
|
+
_REPOS_SEEN[key] = (time.monotonic(), tuple(out))
|
|
1059
|
+
return out
|
|
1012
1060
|
|
|
1013
1061
|
|
|
1014
1062
|
def sub_repositories(root: str) -> list:
|
|
1015
|
-
"""The repositories
|
|
1016
|
-
|
|
1017
|
-
try:
|
|
1018
|
-
names = sorted(os.listdir(root))
|
|
1019
|
-
except OSError:
|
|
1020
|
-
return []
|
|
1021
|
-
out = []
|
|
1022
|
-
for name in names:
|
|
1023
|
-
full = os.path.join(root, name)
|
|
1024
|
-
if name.startswith(".") or os.path.islink(full) or not os.path.isdir(full):
|
|
1025
|
-
continue
|
|
1026
|
-
try:
|
|
1027
|
-
if ".git" in os.listdir(full):
|
|
1028
|
-
out.append(name)
|
|
1029
|
-
except OSError:
|
|
1030
|
-
continue
|
|
1031
|
-
return out[:SUB_REPOS_MAX]
|
|
1063
|
+
"""The repositories inside a folder that is none itself (``find_repositories``), sorted, bounded."""
|
|
1064
|
+
return find_repositories(root)[:SUB_REPOS_MAX]
|
|
1032
1065
|
|
|
1033
1066
|
|
|
1034
1067
|
def is_repository(root: str) -> bool:
|
|
1035
|
-
"""A repository the code graph may hold: ``.git``
|
|
1036
|
-
|
|
1068
|
+
"""A repository the code graph may hold as ONE: ``.git`` at the root; without it, a folder holding repositories
|
|
1069
|
+
is a folder of them (each synced on its own -- even with a manifest or loose scripts at the top, report 06.11
|
|
1070
|
+
#2); otherwise any ecosystem's manifest makes it a project without version control. A home folder or a folder
|
|
1071
|
+
of projects is none (owner 2026-10-25)."""
|
|
1037
1072
|
try:
|
|
1038
1073
|
names = set(os.listdir(root))
|
|
1039
1074
|
except OSError:
|
|
1040
1075
|
return False
|
|
1076
|
+
if ".git" in names:
|
|
1077
|
+
return True
|
|
1078
|
+
if find_repositories(root):
|
|
1079
|
+
return False
|
|
1041
1080
|
return indexer.is_project_root(names)
|
|
1042
1081
|
|
|
1043
1082
|
|
|
1083
|
+
def repo_of(rel: str, repos) -> str:
|
|
1084
|
+
"""The repository of a folder-relative path: the longest repository path it lies under, or ""."""
|
|
1085
|
+
rel = str(rel).replace(os.sep, "/").lstrip("./")
|
|
1086
|
+
best = ""
|
|
1087
|
+
for r in repos or ():
|
|
1088
|
+
if (rel == r or rel.startswith(r + "/")) and len(r) > len(best):
|
|
1089
|
+
best = r
|
|
1090
|
+
return best
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
# the vault's keys of the repositories this machine last synced (a new session answers from the cloud graph while
|
|
1094
|
+
# its own first sync still runs -- report 06.11 #2: a whole session ran on the local index)
|
|
1095
|
+
_KEYS_FILE = os.path.join(os.path.expanduser("~"), ".local", "share", "webbee", "vault-keys.json")
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def remembered_keys(root: str) -> dict:
|
|
1099
|
+
"""``{repo_key | repos, synced_at}`` the vault last confirmed for ``root`` on this machine, or {}."""
|
|
1100
|
+
try:
|
|
1101
|
+
with open(_KEYS_FILE, encoding="utf-8") as f:
|
|
1102
|
+
got = json.load(f).get(os.path.realpath(root)) or {}
|
|
1103
|
+
return got if isinstance(got, dict) and (got.get("repo_key") or got.get("repos")) else {}
|
|
1104
|
+
except (OSError, ValueError, AttributeError):
|
|
1105
|
+
return {}
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def remember_keys(root: str, out: dict) -> None:
|
|
1109
|
+
keys = {k: out[k] for k in ("repo_key", "repos") if out.get(k)}
|
|
1110
|
+
if not keys:
|
|
1111
|
+
return
|
|
1112
|
+
try:
|
|
1113
|
+
try:
|
|
1114
|
+
with open(_KEYS_FILE, encoding="utf-8") as f:
|
|
1115
|
+
data = json.load(f)
|
|
1116
|
+
data = data if isinstance(data, dict) else {}
|
|
1117
|
+
except (OSError, ValueError):
|
|
1118
|
+
data = {}
|
|
1119
|
+
data[os.path.realpath(root)] = {**keys, "synced_at": time.time()}
|
|
1120
|
+
if len(data) > 200: # bounded: the newest roots
|
|
1121
|
+
data = dict(sorted(data.items(), key=lambda kv: -float((kv[1] or {}).get("synced_at") or 0))[:200])
|
|
1122
|
+
os.makedirs(os.path.dirname(_KEYS_FILE), exist_ok=True)
|
|
1123
|
+
tmp = _KEYS_FILE + ".tmp"
|
|
1124
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
1125
|
+
json.dump(data, f)
|
|
1126
|
+
os.replace(tmp, _KEYS_FILE)
|
|
1127
|
+
except OSError:
|
|
1128
|
+
pass
|
|
1129
|
+
|
|
1130
|
+
|
|
1044
1131
|
_MACHINE_ID_FILE = os.path.join(os.path.expanduser("~"), ".local", "share", "webbee", "machine-id")
|
|
1045
1132
|
|
|
1046
1133
|
|
|
@@ -551,8 +551,11 @@ class LocalToolExecutor:
|
|
|
551
551
|
parts.append("defines " + ", ".join(names[:2]) + extra)
|
|
552
552
|
graph = getattr(self.indexer, "graph", None)
|
|
553
553
|
if graph is not None and fi.symbols:
|
|
554
|
-
|
|
555
|
-
|
|
554
|
+
# the files whose references RESOLVE to this file (the vault's rules) -- never a file that only
|
|
555
|
+
# shares a name with one of its definitions (report 06.11 #2: "used by check-paths-agree.py, +50")
|
|
556
|
+
from webbee.intel.query import linker
|
|
557
|
+
deps = sorted({src for src, _u, _k, _l, dst, _q, _o in linker(self.indexer).links()
|
|
558
|
+
if dst == rel and src != rel})
|
|
556
559
|
if deps:
|
|
557
560
|
extra = f" +{len(deps) - 3} more" if len(deps) > 3 else ""
|
|
558
561
|
parts.append("↔ used by " + ", ".join(deps[:3]) + extra)
|
|
@@ -306,7 +306,7 @@ def test_sync_now_waits_for_the_real_sync_and_answers_with_its_confirmation(tmp_
|
|
|
306
306
|
kernel's request waits for its own sync to end and answers with what the vault took -- never "busy"."""
|
|
307
307
|
import threading
|
|
308
308
|
svc, slow = _service_with_slow_sync(tmp_path, monkeypatch)
|
|
309
|
-
svc.
|
|
309
|
+
svc._sync_taken = svc._sync_done = 1 # not the first sync: a run already ended
|
|
310
310
|
svc.sync_vault(None)
|
|
311
311
|
assert slow.started.wait(5)
|
|
312
312
|
got = {}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Report 06.11 #2: a folder whose repositories sit one level deeper (core/<repo>, modules/<repo>) with loose
|
|
2
|
+
scripts at its top was one 670-file project; the first sync answered "pending" for a whole session; the read
|
|
3
|
+
header and graph_slice linked files by a shared name."""
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from webbee.intel import service as sv
|
|
12
|
+
from webbee.intel.service import IntelService, find_repositories, is_repository, repo_of, sub_repositories
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _repo(path):
|
|
16
|
+
os.makedirs(path, exist_ok=True)
|
|
17
|
+
os.makedirs(os.path.join(path, ".git"), exist_ok=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture(autouse=True)
|
|
21
|
+
def _fresh(monkeypatch, tmp_path):
|
|
22
|
+
monkeypatch.setattr(sv, "_REPOS_SEEN", {})
|
|
23
|
+
monkeypatch.setattr(sv, "_KEYS_FILE", str(tmp_path / "keys" / "vault-keys.json"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_repositories_one_level_deeper_are_found_and_make_a_folder(tmp_path):
|
|
27
|
+
for r in ("core/wpanel-sdk", "modules/wpanel-tickets", "gateway"):
|
|
28
|
+
_repo(tmp_path / r)
|
|
29
|
+
_repo(tmp_path / "core/wpanel-sdk/vendor/inner") # inside a repository: never its own unit
|
|
30
|
+
(tmp_path / "requirements.txt").write_text("x\n") # a manifest and loose scripts at the top
|
|
31
|
+
(tmp_path / "check-paths-agree.py").write_text("print(1)\n")
|
|
32
|
+
assert find_repositories(str(tmp_path)) == ["gateway", "core/wpanel-sdk", "modules/wpanel-tickets"]
|
|
33
|
+
assert not is_repository(str(tmp_path)) # a folder of repositories, not one project
|
|
34
|
+
assert sorted(sub_repositories(str(tmp_path))) == ["core/wpanel-sdk", "gateway", "modules/wpanel-tickets"]
|
|
35
|
+
assert repo_of("core/wpanel-sdk/src/a.py", sub_repositories(str(tmp_path))) == "core/wpanel-sdk"
|
|
36
|
+
assert repo_of("check-paths-agree.py", sub_repositories(str(tmp_path))) == ""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_a_manifest_without_repositories_inside_is_a_project(tmp_path):
|
|
40
|
+
(tmp_path / "pyproject.toml").write_text("[project]\nname='x'\n")
|
|
41
|
+
assert is_repository(str(tmp_path))
|
|
42
|
+
_repo(tmp_path / "solo")
|
|
43
|
+
assert is_repository(str(tmp_path / "solo"))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _Slow:
|
|
47
|
+
def __init__(self, out=None):
|
|
48
|
+
self.started, self.release, self.calls, self.out = threading.Event(), threading.Event(), [], out
|
|
49
|
+
|
|
50
|
+
def __call__(self, rels=None, history=True):
|
|
51
|
+
self.calls.append((rels, history))
|
|
52
|
+
self.started.set()
|
|
53
|
+
self.release.wait(10)
|
|
54
|
+
return self.out if self.out is not None else {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _svc(tmp_path, monkeypatch, out=None):
|
|
58
|
+
svc = IntelService(str(tmp_path), "rk", cache_dir=str(tmp_path / "c"))
|
|
59
|
+
svc.vault_client_factory = lambda: object()
|
|
60
|
+
slow = _Slow(out)
|
|
61
|
+
monkeypatch.setattr(svc, "_sync_vault", slow)
|
|
62
|
+
return svc, slow
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_the_first_sync_running_answers_with_the_keys_confirmed_last_time(tmp_path, monkeypatch):
|
|
66
|
+
sv.remember_keys(str(tmp_path), {"repo_key": "", "repos": {"gateway": "k1", "core/wpanel-sdk": "k2"}})
|
|
67
|
+
svc, slow = _svc(tmp_path, monkeypatch)
|
|
68
|
+
svc.sync_vault(None)
|
|
69
|
+
assert slow.started.wait(5)
|
|
70
|
+
got = svc.sync_now()
|
|
71
|
+
assert "pending" in got and got["known"]["repos"] == {"gateway": "k1", "core/wpanel-sdk": "k2"}
|
|
72
|
+
assert "cloud graph answers" in got["pending"]
|
|
73
|
+
slow.release.set()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_pending_ends_with_the_first_run_even_when_it_failed(tmp_path, monkeypatch):
|
|
77
|
+
"""Every tool call queued a sync; the kernel's ask always met one running and 'pending' never ended."""
|
|
78
|
+
svc, slow = _svc(tmp_path, monkeypatch, out={"repo_key": "", "partial_reason": "no repository here"})
|
|
79
|
+
slow.release.set()
|
|
80
|
+
svc.sync_vault(None)
|
|
81
|
+
deadline = time.time() + 5
|
|
82
|
+
while svc._sync_done == 0:
|
|
83
|
+
assert time.time() < deadline
|
|
84
|
+
time.sleep(0.01)
|
|
85
|
+
slow.release.clear()
|
|
86
|
+
svc.sync_vault(None) # the next tool call's sync is running
|
|
87
|
+
assert slow.started.wait(5)
|
|
88
|
+
got = {}
|
|
89
|
+
th = threading.Thread(target=lambda: got.update(svc.sync_now()))
|
|
90
|
+
th.start()
|
|
91
|
+
time.sleep(0.2)
|
|
92
|
+
assert th.is_alive() and not got # waits for a real outcome, never "pending" again
|
|
93
|
+
slow.release.set()
|
|
94
|
+
th.join(10)
|
|
95
|
+
assert got.get("ok") is False and "no repository here" in got["content"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_graph_slice_callers_are_resolved_users(tmp_path):
|
|
99
|
+
pytest.importorskip("tree_sitter_python")
|
|
100
|
+
(tmp_path / "lib.py").write_text("def session_for_sid(sid):\n return sid\n")
|
|
101
|
+
(tmp_path / "api.py").write_text("from lib import session_for_sid\n\n\ndef me(sid):\n"
|
|
102
|
+
" return session_for_sid(sid)\n")
|
|
103
|
+
(tmp_path / "other.py").write_text("def unrelated():\n return 'session_for_sid'\n")
|
|
104
|
+
subprocess.run(["git", "init", "-q", str(tmp_path)], check=False)
|
|
105
|
+
svc = IntelService(str(tmp_path), "rk", cache_dir=str(tmp_path / "c"))
|
|
106
|
+
svc.build()
|
|
107
|
+
from webbee.intel.query import graph_slice
|
|
108
|
+
item = graph_slice(svc, ["session_for_sid"])["data"]["items"][0]
|
|
109
|
+
assert [(c["path"], c["user"], c["line"]) for c in item["callers"]] == [("api.py", "me", 5)]
|
|
@@ -538,13 +538,20 @@ def test_read_header_enriched_with_graph_context(tmp_path):
|
|
|
538
538
|
Symbol("helper", "function", "a.py", 4, 5)]),
|
|
539
539
|
"b.py": FileIndex(path="b.py", lang="python", refs=["Alpha"]),
|
|
540
540
|
})
|
|
541
|
-
class _Svc: # duck-typed IntelService: .index + .graph is all the header reads
|
|
541
|
+
class _Svc: # duck-typed IntelService: .root + .index + .graph is all the header reads
|
|
542
|
+
root = str(tmp_path)
|
|
542
543
|
index = idx
|
|
543
544
|
graph = CodeGraph(idx)
|
|
544
545
|
ex = LocalToolExecutor(str(tmp_path), indexer=_Svc())
|
|
545
546
|
header = ex.run("read_file", {"path": "a.py"})["content"].split("\n", 1)[0]
|
|
546
547
|
assert "defines Alpha, helper" in header
|
|
547
548
|
assert "↔ used by b.py" in header
|
|
549
|
+
# 06.11 #2: a file that only shares a name is no user (it was "used by check-paths-agree.py, +50 more")
|
|
550
|
+
(tmp_path / "c.py").write_text("print('Alpha')\nAlpha = 1\n")
|
|
551
|
+
idx.files["c.py"] = FileIndex(path="c.py", lang="python", refs=["Alpha"])
|
|
552
|
+
_Svc.graph = CodeGraph(idx)
|
|
553
|
+
header = ex.run("read_file", {"path": "a.py"})["content"].split("\n", 1)[0]
|
|
554
|
+
assert "↔ used by b.py" in header and "c.py" not in header
|
|
548
555
|
assert "5 lines" in header # cheap metadata still present
|
|
549
556
|
|
|
550
557
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.7.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|