know-cli 0.3.1__tar.gz → 0.3.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.
- {know_cli-0.3.1 → know_cli-0.3.3}/PKG-INFO +3 -1
- {know_cli-0.3.1 → know_cli-0.3.3}/pyproject.toml +3 -1
- know_cli-0.3.3/src/know/__init__.py +26 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/cli.py +20 -2
- know_cli-0.3.3/src/know/import_graph.py +337 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/semantic_search.py +12 -0
- know_cli-0.3.1/src/know/__init__.py +0 -8
- know_cli-0.3.1/src/know/import_graph.py +0 -218
- {know_cli-0.3.1 → know_cli-0.3.3}/.github/actions/know-cli/action.yml +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/.github/workflows/example-advanced.yml +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/.github/workflows/example-basic.yml +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/.gitignore +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/AGENTS.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/KNOW_SKILL.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/README.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/docs/IMPLEMENTATION_PLAN.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/docs/IMPROVEMENTS.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/docs/STRATEGIC_AUDIT.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/docs/arc.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/docs/architecture-diff.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/docs/architecture.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/docs/dependencies.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/docs/digest-compact.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/docs/digest-llm.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/docs/onboarding-new-devs.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/docs/onboarding-new_devs.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/ai.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/config.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/context_engine.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/cost.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/diff.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/exceptions.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/generator.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/git_hooks.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/history.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/index.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/knowledge_base.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/lexical_index.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/logger.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/mcp_server.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/models.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/parsers.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/quality.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/scanner.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/state.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/stats.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/token_counter.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/tools.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/src/know/watcher.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/README.md +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/conftest.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/mcp_stub/mcp/__init__.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/mcp_stub/mcp/server/__init__.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/mcp_stub/mcp/server/fastmcp.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_efficiency.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_history_cost.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_know.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_search_hybrid.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_state.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_unit.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_week2.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_week3.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_week4.py +0 -0
- {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_week6_tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: know-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Context Intelligence for AI Coding Agents — smart, token-budgeted code context
|
|
5
5
|
Project-URL: Homepage, https://github.com/vic/know-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/vic/know-cli
|
|
@@ -19,7 +19,9 @@ Classifier: Topic :: Utilities
|
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Requires-Dist: anthropic>=0.8.0
|
|
21
21
|
Requires-Dist: click>=8.1.0
|
|
22
|
+
Requires-Dist: fastembed>=0.3.0
|
|
22
23
|
Requires-Dist: httpx>=0.24.0
|
|
24
|
+
Requires-Dist: numpy>=1.24.0
|
|
23
25
|
Requires-Dist: pathspec>=0.11.0
|
|
24
26
|
Requires-Dist: pyyaml>=6.0
|
|
25
27
|
Requires-Dist: rich>=13.0.0
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "know-cli"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Context Intelligence for AI Coding Agents — smart, token-budgeted code context"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -33,6 +33,8 @@ dependencies = [
|
|
|
33
33
|
"httpx>=0.24.0",
|
|
34
34
|
"pathspec>=0.11.0",
|
|
35
35
|
"xxhash>=3.0.0",
|
|
36
|
+
"fastembed>=0.3.0",
|
|
37
|
+
"numpy>=1.24.0",
|
|
36
38
|
]
|
|
37
39
|
|
|
38
40
|
[project.optional-dependencies]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""know - Context Intelligence for codebases."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__author__ = "Vic"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _get_version() -> str:
|
|
9
|
+
"""Best-effort runtime version.
|
|
10
|
+
|
|
11
|
+
Avoids hardcoding so `know --version` always matches the installed dist.
|
|
12
|
+
"""
|
|
13
|
+
try:
|
|
14
|
+
from importlib import metadata as _md # py3.8+
|
|
15
|
+
|
|
16
|
+
return _md.version("know-cli")
|
|
17
|
+
except Exception:
|
|
18
|
+
# Fallback for editable/dev installs or very old envs
|
|
19
|
+
return "0.0.0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__version__ = _get_version()
|
|
23
|
+
|
|
24
|
+
from know.cli import main # noqa: E402
|
|
25
|
+
|
|
26
|
+
__all__ = ["main", "__version__"]
|
|
@@ -739,7 +739,16 @@ def search(
|
|
|
739
739
|
"""Search code using hybrid retrieval (BM25 + vectors) with deterministic reranking."""
|
|
740
740
|
config = ctx.obj["config"]
|
|
741
741
|
|
|
742
|
-
|
|
742
|
+
try:
|
|
743
|
+
from know.semantic_search import SemanticSearcher
|
|
744
|
+
except ModuleNotFoundError as e:
|
|
745
|
+
missing = getattr(e, "name", "")
|
|
746
|
+
if missing in {"numpy", "fastembed"}:
|
|
747
|
+
raise click.ClickException(
|
|
748
|
+
"Search dependencies missing. Install with: pip install 'know-cli[search]' "
|
|
749
|
+
"(or pip install numpy fastembed)."
|
|
750
|
+
)
|
|
751
|
+
raise
|
|
743
752
|
|
|
744
753
|
# Back-compat: --chunk historically meant vector chunk search.
|
|
745
754
|
if chunk:
|
|
@@ -1133,7 +1142,16 @@ def reindex(ctx: click.Context, chunks: bool, file_level: bool) -> None:
|
|
|
1133
1142
|
"""
|
|
1134
1143
|
config = ctx.obj["config"]
|
|
1135
1144
|
|
|
1136
|
-
|
|
1145
|
+
try:
|
|
1146
|
+
from know.semantic_search import SemanticSearcher
|
|
1147
|
+
except ModuleNotFoundError as e:
|
|
1148
|
+
missing = getattr(e, "name", "")
|
|
1149
|
+
if missing in {"numpy", "fastembed"}:
|
|
1150
|
+
raise click.ClickException(
|
|
1151
|
+
"Search dependencies missing. Install with: pip install 'know-cli[search]' "
|
|
1152
|
+
"(or pip install numpy fastembed)."
|
|
1153
|
+
)
|
|
1154
|
+
raise
|
|
1137
1155
|
|
|
1138
1156
|
if not ctx.obj.get("quiet"):
|
|
1139
1157
|
console.print("[dim]Clearing existing embeddings...[/dim]")
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Import graph: tracks dependencies between project modules.
|
|
2
|
+
|
|
3
|
+
Stores import relationships as an adjacency list in index.db.
|
|
4
|
+
Supports queries for 'what does X import?' and 'what imports X?'.
|
|
5
|
+
Now supports Python + TypeScript/JavaScript import edges.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
import re
|
|
10
|
+
import sqlite3
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional, Set, Tuple, TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from know.logger import get_logger
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from know.config import Config
|
|
18
|
+
|
|
19
|
+
logger = get_logger()
|
|
20
|
+
|
|
21
|
+
_TS_EXTS = {".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"}
|
|
22
|
+
_PY_EXTS = {".py"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ImportGraph:
|
|
26
|
+
"""Builds and queries the import dependency graph for a project.
|
|
27
|
+
|
|
28
|
+
Edges are stored in SQLite alongside the existing index.db.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, config: "Config"):
|
|
32
|
+
self.config = config
|
|
33
|
+
self.root = config.root
|
|
34
|
+
self.db_path = config.root / ".know" / "cache" / "index.db"
|
|
35
|
+
self._conn: Optional[sqlite3.Connection] = None
|
|
36
|
+
self._ensure_table()
|
|
37
|
+
|
|
38
|
+
def _get_conn(self) -> sqlite3.Connection:
|
|
39
|
+
if self._conn is None:
|
|
40
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
self._conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
42
|
+
return self._conn
|
|
43
|
+
|
|
44
|
+
def close(self):
|
|
45
|
+
if self._conn:
|
|
46
|
+
self._conn.close()
|
|
47
|
+
self._conn = None
|
|
48
|
+
|
|
49
|
+
def __enter__(self):
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
def __exit__(self, *exc):
|
|
53
|
+
self.close()
|
|
54
|
+
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
# Schema
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
def _ensure_table(self):
|
|
59
|
+
conn = self._get_conn()
|
|
60
|
+
conn.executescript(
|
|
61
|
+
"""
|
|
62
|
+
CREATE TABLE IF NOT EXISTS import_edges (
|
|
63
|
+
source TEXT NOT NULL,
|
|
64
|
+
target TEXT NOT NULL,
|
|
65
|
+
import_type TEXT NOT NULL DEFAULT 'import',
|
|
66
|
+
PRIMARY KEY (source, target)
|
|
67
|
+
);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_import_source ON import_edges(source);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_import_target ON import_edges(target);
|
|
70
|
+
"""
|
|
71
|
+
)
|
|
72
|
+
conn.commit()
|
|
73
|
+
|
|
74
|
+
# ------------------------------------------------------------------
|
|
75
|
+
# Build graph from codebase
|
|
76
|
+
# ------------------------------------------------------------------
|
|
77
|
+
def build(self, modules: Optional[list] = None) -> int:
|
|
78
|
+
"""Build the full import graph from project modules.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
modules: Optional list of ModuleInfo dicts/objects (from scanner).
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Number of edges inserted.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
module_to_path: Dict[str, Path] = {}
|
|
88
|
+
known_modules: Dict[str, str] = {}
|
|
89
|
+
stem_index: Dict[str, Set[str]] = {}
|
|
90
|
+
|
|
91
|
+
if modules:
|
|
92
|
+
for m in modules:
|
|
93
|
+
path_str = m["path"] if isinstance(m, dict) else str(m.path)
|
|
94
|
+
name = m["name"] if isinstance(m, dict) else m.name
|
|
95
|
+
path = self.root / path_str
|
|
96
|
+
module_to_path[name] = path
|
|
97
|
+
known_modules[name] = name
|
|
98
|
+
|
|
99
|
+
stem = Path(path_str).stem
|
|
100
|
+
stem_index.setdefault(stem, set()).add(name)
|
|
101
|
+
else:
|
|
102
|
+
for path in self.root.rglob("*"):
|
|
103
|
+
if not path.is_file():
|
|
104
|
+
continue
|
|
105
|
+
if path.suffix not in (_PY_EXTS | _TS_EXTS):
|
|
106
|
+
continue
|
|
107
|
+
if any(p.startswith(".") or p in {"venv", "node_modules", "__pycache__", ".git"} for p in path.parts):
|
|
108
|
+
continue
|
|
109
|
+
rel = path.relative_to(self.root)
|
|
110
|
+
name = str(rel.with_suffix("")).replace("/", ".")
|
|
111
|
+
module_to_path[name] = path
|
|
112
|
+
known_modules[name] = name
|
|
113
|
+
stem_index.setdefault(path.stem, set()).add(name)
|
|
114
|
+
|
|
115
|
+
edges: List[Tuple[str, str, str]] = []
|
|
116
|
+
|
|
117
|
+
for source_module, abs_path in module_to_path.items():
|
|
118
|
+
if not abs_path.exists():
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
text = abs_path.read_text(encoding="utf-8", errors="ignore")
|
|
123
|
+
except Exception:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
if abs_path.suffix in _PY_EXTS:
|
|
127
|
+
refs = self._extract_python_imports(text)
|
|
128
|
+
elif abs_path.suffix in _TS_EXTS:
|
|
129
|
+
refs = self._extract_ts_imports(text)
|
|
130
|
+
else:
|
|
131
|
+
refs = []
|
|
132
|
+
|
|
133
|
+
for raw_target, import_type in refs:
|
|
134
|
+
resolved = self._resolve_target(
|
|
135
|
+
raw_target=raw_target,
|
|
136
|
+
source_module=source_module,
|
|
137
|
+
source_path=abs_path,
|
|
138
|
+
known_modules=known_modules,
|
|
139
|
+
stem_index=stem_index,
|
|
140
|
+
)
|
|
141
|
+
if not resolved or resolved == source_module:
|
|
142
|
+
continue
|
|
143
|
+
edges.append((source_module, resolved, import_type))
|
|
144
|
+
|
|
145
|
+
conn = self._get_conn()
|
|
146
|
+
conn.execute("DELETE FROM import_edges")
|
|
147
|
+
if edges:
|
|
148
|
+
conn.executemany(
|
|
149
|
+
"INSERT OR REPLACE INTO import_edges (source, target, import_type) VALUES (?, ?, ?)",
|
|
150
|
+
edges,
|
|
151
|
+
)
|
|
152
|
+
conn.commit()
|
|
153
|
+
return len(edges)
|
|
154
|
+
|
|
155
|
+
def _extract_python_imports(self, source: str) -> List[Tuple[str, str]]:
|
|
156
|
+
refs: List[Tuple[str, str]] = []
|
|
157
|
+
try:
|
|
158
|
+
tree = ast.parse(source)
|
|
159
|
+
except Exception:
|
|
160
|
+
return refs
|
|
161
|
+
|
|
162
|
+
for node in ast.walk(tree):
|
|
163
|
+
if isinstance(node, ast.Import):
|
|
164
|
+
for alias in node.names:
|
|
165
|
+
refs.append((alias.name, "import"))
|
|
166
|
+
elif isinstance(node, ast.ImportFrom):
|
|
167
|
+
if node.module:
|
|
168
|
+
if getattr(node, "level", 0) and node.level > 0:
|
|
169
|
+
dotted = "." * node.level + node.module
|
|
170
|
+
refs.append((dotted, "from"))
|
|
171
|
+
else:
|
|
172
|
+
refs.append((node.module, "from"))
|
|
173
|
+
return refs
|
|
174
|
+
|
|
175
|
+
def _extract_ts_imports(self, source: str) -> List[Tuple[str, str]]:
|
|
176
|
+
refs: List[Tuple[str, str]] = []
|
|
177
|
+
|
|
178
|
+
patterns = [
|
|
179
|
+
r"import\s+(?:[^'\"]+?\s+from\s+)?['\"]([^'\"]+)['\"]", # import ... from 'x' / import 'x'
|
|
180
|
+
r"export\s+[^'\"]*?from\s+['\"]([^'\"]+)['\"]", # export ... from 'x'
|
|
181
|
+
r"require\(\s*['\"]([^'\"]+)['\"]\s*\)", # require('x')
|
|
182
|
+
r"import\(\s*['\"]([^'\"]+)['\"]\s*\)", # import('x')
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
seen: Set[str] = set()
|
|
186
|
+
for pattern in patterns:
|
|
187
|
+
for m in re.finditer(pattern, source):
|
|
188
|
+
mod = m.group(1)
|
|
189
|
+
if mod and mod not in seen:
|
|
190
|
+
seen.add(mod)
|
|
191
|
+
refs.append((mod, "import"))
|
|
192
|
+
return refs
|
|
193
|
+
|
|
194
|
+
def _resolve_target(
|
|
195
|
+
self,
|
|
196
|
+
raw_target: str,
|
|
197
|
+
source_module: str,
|
|
198
|
+
source_path: Path,
|
|
199
|
+
known_modules: Dict[str, str],
|
|
200
|
+
stem_index: Dict[str, Set[str]],
|
|
201
|
+
) -> Optional[str]:
|
|
202
|
+
# Relative imports: ./foo, ../bar, .utils, ..pkg.mod
|
|
203
|
+
if raw_target.startswith(("./", "../", ".")):
|
|
204
|
+
rel_candidate = self._resolve_relative(raw_target, source_path)
|
|
205
|
+
if rel_candidate:
|
|
206
|
+
rel_mod = str(rel_candidate.relative_to(self.root).with_suffix("")).replace("/", ".")
|
|
207
|
+
if rel_mod in known_modules:
|
|
208
|
+
return rel_mod
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
# Exact module path known
|
|
212
|
+
if raw_target in known_modules:
|
|
213
|
+
return known_modules[raw_target]
|
|
214
|
+
|
|
215
|
+
# Try dotted last token / package token
|
|
216
|
+
token = raw_target.split("/")[-1].split(".")[-1]
|
|
217
|
+
if token in stem_index and stem_index[token]:
|
|
218
|
+
# Prefer shortest match (usually most local path)
|
|
219
|
+
return sorted(stem_index[token], key=len)[0]
|
|
220
|
+
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
def _resolve_relative(self, raw_target: str, source_path: Path) -> Optional[Path]:
|
|
224
|
+
base = source_path.parent
|
|
225
|
+
|
|
226
|
+
# Python dotted relative: .utils / ..pkg.mod
|
|
227
|
+
if raw_target.startswith(".") and not raw_target.startswith("./") and "/" not in raw_target:
|
|
228
|
+
level = len(raw_target) - len(raw_target.lstrip("."))
|
|
229
|
+
tail = raw_target[level:]
|
|
230
|
+
base_dir = source_path.parent
|
|
231
|
+
for _ in range(max(level - 1, 0)):
|
|
232
|
+
base_dir = base_dir.parent
|
|
233
|
+
candidate = base_dir / tail.replace(".", "/")
|
|
234
|
+
else:
|
|
235
|
+
candidate = (base / raw_target).resolve()
|
|
236
|
+
|
|
237
|
+
candidates = [
|
|
238
|
+
candidate,
|
|
239
|
+
candidate.with_suffix(".py"),
|
|
240
|
+
candidate.with_suffix(".ts"),
|
|
241
|
+
candidate.with_suffix(".tsx"),
|
|
242
|
+
candidate.with_suffix(".js"),
|
|
243
|
+
candidate.with_suffix(".jsx"),
|
|
244
|
+
candidate / "__init__.py",
|
|
245
|
+
candidate / "index.ts",
|
|
246
|
+
candidate / "index.tsx",
|
|
247
|
+
candidate / "index.js",
|
|
248
|
+
candidate / "index.jsx",
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
for c in candidates:
|
|
252
|
+
try:
|
|
253
|
+
if c.exists() and c.is_file() and self.root in c.parents:
|
|
254
|
+
return c
|
|
255
|
+
except Exception:
|
|
256
|
+
continue
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
# ------------------------------------------------------------------
|
|
260
|
+
# Queries
|
|
261
|
+
# ------------------------------------------------------------------
|
|
262
|
+
def imports_of(self, module_name: str) -> List[str]:
|
|
263
|
+
"""What does *module_name* import? (outgoing edges)"""
|
|
264
|
+
conn = self._get_conn()
|
|
265
|
+
short = module_name.split(".")[-1]
|
|
266
|
+
rows = conn.execute(
|
|
267
|
+
"SELECT target FROM import_edges WHERE source = ? OR source LIKE ?",
|
|
268
|
+
(module_name, f"%.{short}"),
|
|
269
|
+
).fetchall()
|
|
270
|
+
return list({r[0] for r in rows})
|
|
271
|
+
|
|
272
|
+
def imported_by(self, module_name: str) -> List[str]:
|
|
273
|
+
"""What modules import *module_name*? (incoming edges)"""
|
|
274
|
+
conn = self._get_conn()
|
|
275
|
+
short = module_name.split(".")[-1]
|
|
276
|
+
rows = conn.execute(
|
|
277
|
+
"SELECT source FROM import_edges WHERE target = ? OR target LIKE ?",
|
|
278
|
+
(module_name, f"%.{short}"),
|
|
279
|
+
).fetchall()
|
|
280
|
+
return list({r[0] for r in rows})
|
|
281
|
+
|
|
282
|
+
def get_all_edges(self) -> List[Tuple[str, str]]:
|
|
283
|
+
"""Return all (source, target) edges."""
|
|
284
|
+
conn = self._get_conn()
|
|
285
|
+
rows = conn.execute("SELECT source, target FROM import_edges").fetchall()
|
|
286
|
+
return [(r[0], r[1]) for r in rows]
|
|
287
|
+
|
|
288
|
+
def file_for_module(self, module_name: str) -> Optional[Path]:
|
|
289
|
+
"""Resolve a module name to a file path under project root."""
|
|
290
|
+
rel = module_name.replace(".", "/")
|
|
291
|
+
candidates = [
|
|
292
|
+
self.root / f"{rel}.py",
|
|
293
|
+
self.root / f"{rel}.ts",
|
|
294
|
+
self.root / f"{rel}.tsx",
|
|
295
|
+
self.root / f"{rel}.js",
|
|
296
|
+
self.root / f"{rel}.jsx",
|
|
297
|
+
]
|
|
298
|
+
for candidate in candidates:
|
|
299
|
+
if candidate.exists():
|
|
300
|
+
return candidate
|
|
301
|
+
|
|
302
|
+
short = module_name.split(".")[-1]
|
|
303
|
+
for ext in ["py", "ts", "tsx", "js", "jsx"]:
|
|
304
|
+
for p in self.root.rglob(f"{short}.{ext}"):
|
|
305
|
+
parts = p.relative_to(self.root).parts
|
|
306
|
+
if any(part.startswith(".") or part in {"venv", "__pycache__", "node_modules", ".git"} for part in parts):
|
|
307
|
+
continue
|
|
308
|
+
return p
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
# ------------------------------------------------------------------
|
|
312
|
+
# Pretty print
|
|
313
|
+
# ------------------------------------------------------------------
|
|
314
|
+
def format_graph(self, module_name: str) -> str:
|
|
315
|
+
"""Human-readable graph display for a single module."""
|
|
316
|
+
imports = self.imports_of(module_name)
|
|
317
|
+
imported = self.imported_by(module_name)
|
|
318
|
+
|
|
319
|
+
lines = [f"# Import graph for: {module_name}", ""]
|
|
320
|
+
|
|
321
|
+
if imports:
|
|
322
|
+
lines.append("## Imports (dependencies)")
|
|
323
|
+
for m in sorted(imports):
|
|
324
|
+
lines.append(f" → {m}")
|
|
325
|
+
else:
|
|
326
|
+
lines.append("## Imports: (none)")
|
|
327
|
+
|
|
328
|
+
lines.append("")
|
|
329
|
+
|
|
330
|
+
if imported:
|
|
331
|
+
lines.append("## Imported by (dependents)")
|
|
332
|
+
for m in sorted(imported):
|
|
333
|
+
lines.append(f" ← {m}")
|
|
334
|
+
else:
|
|
335
|
+
lines.append("## Imported by: (none)")
|
|
336
|
+
|
|
337
|
+
return "\n".join(lines)
|
|
@@ -646,6 +646,18 @@ class SemanticSearcher:
|
|
|
646
646
|
if path.startswith("docs/") or path.startswith("tests/"):
|
|
647
647
|
score *= 0.7
|
|
648
648
|
|
|
649
|
+
# Filename/path lexical boost: if query tokens appear in filename/path,
|
|
650
|
+
# prioritize those hits (helps frontend queries like "composer toolbar").
|
|
651
|
+
try:
|
|
652
|
+
q_tokens = [t.lower() for t in query.replace("_", " ").replace("-", " ").split() if len(t) >= 3]
|
|
653
|
+
path_l = str(path).lower().replace("_", " ").replace("-", " ")
|
|
654
|
+
if q_tokens:
|
|
655
|
+
matches = sum(1 for t in q_tokens if t in path_l)
|
|
656
|
+
if matches > 0:
|
|
657
|
+
score *= 1.0 + min(0.35, 0.12 * matches)
|
|
658
|
+
except Exception:
|
|
659
|
+
pass
|
|
660
|
+
|
|
649
661
|
out = {
|
|
650
662
|
"schema": "know.search.v1",
|
|
651
663
|
"engine": engine,
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
"""Import graph: tracks dependencies between project modules.
|
|
2
|
-
|
|
3
|
-
Stores import relationships as an adjacency list in index.db.
|
|
4
|
-
Provides queries for 'what does X import?' and 'what imports X?'.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import ast
|
|
8
|
-
import sqlite3
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Dict, List, Optional, Set, Tuple, TYPE_CHECKING
|
|
11
|
-
import logging
|
|
12
|
-
|
|
13
|
-
from know.logger import get_logger
|
|
14
|
-
|
|
15
|
-
if TYPE_CHECKING:
|
|
16
|
-
from know.config import Config
|
|
17
|
-
|
|
18
|
-
logger = get_logger()
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ImportGraph:
|
|
22
|
-
"""Builds and queries the import dependency graph for a project.
|
|
23
|
-
|
|
24
|
-
Edges are stored in SQLite alongside the existing index.db.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
def __init__(self, config: "Config"):
|
|
28
|
-
self.config = config
|
|
29
|
-
self.root = config.root
|
|
30
|
-
self.db_path = config.root / ".know" / "cache" / "index.db"
|
|
31
|
-
self._conn: Optional[sqlite3.Connection] = None
|
|
32
|
-
self._ensure_table()
|
|
33
|
-
|
|
34
|
-
def _get_conn(self) -> sqlite3.Connection:
|
|
35
|
-
if self._conn is None:
|
|
36
|
-
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
-
self._conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
38
|
-
return self._conn
|
|
39
|
-
|
|
40
|
-
def close(self):
|
|
41
|
-
if self._conn:
|
|
42
|
-
self._conn.close()
|
|
43
|
-
self._conn = None
|
|
44
|
-
|
|
45
|
-
def __enter__(self):
|
|
46
|
-
return self
|
|
47
|
-
|
|
48
|
-
def __exit__(self, *exc):
|
|
49
|
-
self.close()
|
|
50
|
-
|
|
51
|
-
# ------------------------------------------------------------------
|
|
52
|
-
# Schema
|
|
53
|
-
# ------------------------------------------------------------------
|
|
54
|
-
def _ensure_table(self):
|
|
55
|
-
conn = self._get_conn()
|
|
56
|
-
conn.executescript("""
|
|
57
|
-
CREATE TABLE IF NOT EXISTS import_edges (
|
|
58
|
-
source TEXT NOT NULL,
|
|
59
|
-
target TEXT NOT NULL,
|
|
60
|
-
import_type TEXT NOT NULL DEFAULT 'import',
|
|
61
|
-
PRIMARY KEY (source, target)
|
|
62
|
-
);
|
|
63
|
-
CREATE INDEX IF NOT EXISTS idx_import_source ON import_edges(source);
|
|
64
|
-
CREATE INDEX IF NOT EXISTS idx_import_target ON import_edges(target);
|
|
65
|
-
""")
|
|
66
|
-
conn.commit()
|
|
67
|
-
|
|
68
|
-
# ------------------------------------------------------------------
|
|
69
|
-
# Build graph from codebase
|
|
70
|
-
# ------------------------------------------------------------------
|
|
71
|
-
def build(self, modules: Optional[list] = None) -> int:
|
|
72
|
-
"""Build the full import graph from the project modules.
|
|
73
|
-
|
|
74
|
-
Args:
|
|
75
|
-
modules: Optional list of ModuleInfo dicts (from scanner).
|
|
76
|
-
If None, scans Python files under self.root.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
Number of edges inserted.
|
|
80
|
-
"""
|
|
81
|
-
# Collect all known module short-names -> full-name mapping
|
|
82
|
-
known_modules: Dict[str, str] = {}
|
|
83
|
-
py_files: Dict[str, Path] = {} # short_name -> abs path
|
|
84
|
-
|
|
85
|
-
if modules:
|
|
86
|
-
for m in modules:
|
|
87
|
-
path_str = m["path"] if isinstance(m, dict) else str(m.path)
|
|
88
|
-
name = m["name"] if isinstance(m, dict) else m.name
|
|
89
|
-
short = name.split(".")[-1]
|
|
90
|
-
known_modules[short] = name
|
|
91
|
-
known_modules[name] = name
|
|
92
|
-
py_files[name] = self.root / path_str
|
|
93
|
-
else:
|
|
94
|
-
# Fall back: discover .py files
|
|
95
|
-
for py in self.root.rglob("*.py"):
|
|
96
|
-
if any(p.startswith(".") or p in {"venv", "node_modules", "__pycache__", ".git"}
|
|
97
|
-
for p in py.parts):
|
|
98
|
-
continue
|
|
99
|
-
try:
|
|
100
|
-
rel = py.relative_to(self.root)
|
|
101
|
-
except ValueError:
|
|
102
|
-
continue
|
|
103
|
-
name = str(rel.with_suffix("")).replace("/", ".")
|
|
104
|
-
short = name.split(".")[-1]
|
|
105
|
-
known_modules[short] = name
|
|
106
|
-
known_modules[name] = name
|
|
107
|
-
py_files[name] = py
|
|
108
|
-
|
|
109
|
-
edges: List[Tuple[str, str, str]] = []
|
|
110
|
-
|
|
111
|
-
for mod_name, abs_path in py_files.items():
|
|
112
|
-
if not abs_path.exists() or not str(abs_path).endswith(".py"):
|
|
113
|
-
continue
|
|
114
|
-
try:
|
|
115
|
-
source = abs_path.read_text(encoding="utf-8", errors="ignore")
|
|
116
|
-
tree = ast.parse(source)
|
|
117
|
-
except (SyntaxError, UnicodeDecodeError):
|
|
118
|
-
continue
|
|
119
|
-
|
|
120
|
-
for node in ast.walk(tree):
|
|
121
|
-
targets: List[Tuple[str, str]] = [] # (resolved_name, type)
|
|
122
|
-
|
|
123
|
-
if isinstance(node, ast.Import):
|
|
124
|
-
for alias in node.names:
|
|
125
|
-
t = alias.name.split(".")[-1]
|
|
126
|
-
if t in known_modules and known_modules[t] != mod_name:
|
|
127
|
-
targets.append((known_modules[t], "import"))
|
|
128
|
-
elif isinstance(node, ast.ImportFrom):
|
|
129
|
-
if node.module:
|
|
130
|
-
t = node.module.split(".")[-1]
|
|
131
|
-
if t in known_modules and known_modules[t] != mod_name:
|
|
132
|
-
targets.append((known_modules[t], "from"))
|
|
133
|
-
|
|
134
|
-
for resolved, imp_type in targets:
|
|
135
|
-
edges.append((mod_name, resolved, imp_type))
|
|
136
|
-
|
|
137
|
-
# Persist
|
|
138
|
-
conn = self._get_conn()
|
|
139
|
-
conn.execute("DELETE FROM import_edges")
|
|
140
|
-
if edges:
|
|
141
|
-
conn.executemany(
|
|
142
|
-
"INSERT OR REPLACE INTO import_edges (source, target, import_type) VALUES (?, ?, ?)",
|
|
143
|
-
edges,
|
|
144
|
-
)
|
|
145
|
-
conn.commit()
|
|
146
|
-
return len(edges)
|
|
147
|
-
|
|
148
|
-
# ------------------------------------------------------------------
|
|
149
|
-
# Queries
|
|
150
|
-
# ------------------------------------------------------------------
|
|
151
|
-
def imports_of(self, module_name: str) -> List[str]:
|
|
152
|
-
"""What does *module_name* import? (outgoing edges)"""
|
|
153
|
-
conn = self._get_conn()
|
|
154
|
-
short = module_name.split(".")[-1]
|
|
155
|
-
rows = conn.execute(
|
|
156
|
-
"SELECT target FROM import_edges WHERE source = ? OR source LIKE ?",
|
|
157
|
-
(module_name, f"%.{short}"),
|
|
158
|
-
).fetchall()
|
|
159
|
-
return list({r[0] for r in rows})
|
|
160
|
-
|
|
161
|
-
def imported_by(self, module_name: str) -> List[str]:
|
|
162
|
-
"""What modules import *module_name*? (incoming edges)"""
|
|
163
|
-
conn = self._get_conn()
|
|
164
|
-
short = module_name.split(".")[-1]
|
|
165
|
-
rows = conn.execute(
|
|
166
|
-
"SELECT source FROM import_edges WHERE target = ? OR target LIKE ?",
|
|
167
|
-
(module_name, f"%.{short}"),
|
|
168
|
-
).fetchall()
|
|
169
|
-
return list({r[0] for r in rows})
|
|
170
|
-
|
|
171
|
-
def get_all_edges(self) -> List[Tuple[str, str]]:
|
|
172
|
-
"""Return all (source, target) edges."""
|
|
173
|
-
conn = self._get_conn()
|
|
174
|
-
rows = conn.execute("SELECT source, target FROM import_edges").fetchall()
|
|
175
|
-
return [(r[0], r[1]) for r in rows]
|
|
176
|
-
|
|
177
|
-
def file_for_module(self, module_name: str) -> Optional[Path]:
|
|
178
|
-
"""Resolve a module name to a file path under project root."""
|
|
179
|
-
# Try direct path conversion
|
|
180
|
-
rel = module_name.replace(".", "/") + ".py"
|
|
181
|
-
candidate = self.root / rel
|
|
182
|
-
if candidate.exists():
|
|
183
|
-
return candidate
|
|
184
|
-
# Try short name search
|
|
185
|
-
short = module_name.split(".")[-1]
|
|
186
|
-
for py in self.root.rglob(f"{short}.py"):
|
|
187
|
-
if not any(p.startswith(".") or p in {"venv", "__pycache__"}
|
|
188
|
-
for p in py.relative_to(self.root).parts):
|
|
189
|
-
return py
|
|
190
|
-
return None
|
|
191
|
-
|
|
192
|
-
# ------------------------------------------------------------------
|
|
193
|
-
# Pretty print
|
|
194
|
-
# ------------------------------------------------------------------
|
|
195
|
-
def format_graph(self, module_name: str) -> str:
|
|
196
|
-
"""Human-readable graph display for a single module."""
|
|
197
|
-
imports = self.imports_of(module_name)
|
|
198
|
-
imported = self.imported_by(module_name)
|
|
199
|
-
|
|
200
|
-
lines = [f"# Import graph for: {module_name}", ""]
|
|
201
|
-
|
|
202
|
-
if imports:
|
|
203
|
-
lines.append("## Imports (dependencies)")
|
|
204
|
-
for m in sorted(imports):
|
|
205
|
-
lines.append(f" → {m}")
|
|
206
|
-
else:
|
|
207
|
-
lines.append("## Imports: (none)")
|
|
208
|
-
|
|
209
|
-
lines.append("")
|
|
210
|
-
|
|
211
|
-
if imported:
|
|
212
|
-
lines.append("## Imported by (dependents)")
|
|
213
|
-
for m in sorted(imported):
|
|
214
|
-
lines.append(f" ← {m}")
|
|
215
|
-
else:
|
|
216
|
-
lines.append("## Imported by: (none)")
|
|
217
|
-
|
|
218
|
-
return "\n".join(lines)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|