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.
Files changed (64) hide show
  1. {know_cli-0.3.1 → know_cli-0.3.3}/PKG-INFO +3 -1
  2. {know_cli-0.3.1 → know_cli-0.3.3}/pyproject.toml +3 -1
  3. know_cli-0.3.3/src/know/__init__.py +26 -0
  4. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/cli.py +20 -2
  5. know_cli-0.3.3/src/know/import_graph.py +337 -0
  6. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/semantic_search.py +12 -0
  7. know_cli-0.3.1/src/know/__init__.py +0 -8
  8. know_cli-0.3.1/src/know/import_graph.py +0 -218
  9. {know_cli-0.3.1 → know_cli-0.3.3}/.github/actions/know-cli/action.yml +0 -0
  10. {know_cli-0.3.1 → know_cli-0.3.3}/.github/workflows/example-advanced.yml +0 -0
  11. {know_cli-0.3.1 → know_cli-0.3.3}/.github/workflows/example-basic.yml +0 -0
  12. {know_cli-0.3.1 → know_cli-0.3.3}/.gitignore +0 -0
  13. {know_cli-0.3.1 → know_cli-0.3.3}/AGENTS.md +0 -0
  14. {know_cli-0.3.1 → know_cli-0.3.3}/KNOW_SKILL.md +0 -0
  15. {know_cli-0.3.1 → know_cli-0.3.3}/README.md +0 -0
  16. {know_cli-0.3.1 → know_cli-0.3.3}/docs/IMPLEMENTATION_PLAN.md +0 -0
  17. {know_cli-0.3.1 → know_cli-0.3.3}/docs/IMPROVEMENTS.md +0 -0
  18. {know_cli-0.3.1 → know_cli-0.3.3}/docs/STRATEGIC_AUDIT.md +0 -0
  19. {know_cli-0.3.1 → know_cli-0.3.3}/docs/arc.md +0 -0
  20. {know_cli-0.3.1 → know_cli-0.3.3}/docs/architecture-diff.md +0 -0
  21. {know_cli-0.3.1 → know_cli-0.3.3}/docs/architecture.md +0 -0
  22. {know_cli-0.3.1 → know_cli-0.3.3}/docs/dependencies.md +0 -0
  23. {know_cli-0.3.1 → know_cli-0.3.3}/docs/digest-compact.md +0 -0
  24. {know_cli-0.3.1 → know_cli-0.3.3}/docs/digest-llm.md +0 -0
  25. {know_cli-0.3.1 → know_cli-0.3.3}/docs/onboarding-new-devs.md +0 -0
  26. {know_cli-0.3.1 → know_cli-0.3.3}/docs/onboarding-new_devs.md +0 -0
  27. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/ai.py +0 -0
  28. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/config.py +0 -0
  29. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/context_engine.py +0 -0
  30. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/cost.py +0 -0
  31. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/diff.py +0 -0
  32. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/exceptions.py +0 -0
  33. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/generator.py +0 -0
  34. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/git_hooks.py +0 -0
  35. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/history.py +0 -0
  36. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/index.py +0 -0
  37. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/knowledge_base.py +0 -0
  38. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/lexical_index.py +0 -0
  39. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/logger.py +0 -0
  40. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/mcp_server.py +0 -0
  41. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/models.py +0 -0
  42. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/parsers.py +0 -0
  43. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/quality.py +0 -0
  44. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/scanner.py +0 -0
  45. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/state.py +0 -0
  46. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/stats.py +0 -0
  47. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/token_counter.py +0 -0
  48. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/tools.py +0 -0
  49. {know_cli-0.3.1 → know_cli-0.3.3}/src/know/watcher.py +0 -0
  50. {know_cli-0.3.1 → know_cli-0.3.3}/tests/README.md +0 -0
  51. {know_cli-0.3.1 → know_cli-0.3.3}/tests/conftest.py +0 -0
  52. {know_cli-0.3.1 → know_cli-0.3.3}/tests/mcp_stub/mcp/__init__.py +0 -0
  53. {know_cli-0.3.1 → know_cli-0.3.3}/tests/mcp_stub/mcp/server/__init__.py +0 -0
  54. {know_cli-0.3.1 → know_cli-0.3.3}/tests/mcp_stub/mcp/server/fastmcp.py +0 -0
  55. {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_efficiency.py +0 -0
  56. {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_history_cost.py +0 -0
  57. {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_know.py +0 -0
  58. {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_search_hybrid.py +0 -0
  59. {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_state.py +0 -0
  60. {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_unit.py +0 -0
  61. {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_week2.py +0 -0
  62. {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_week3.py +0 -0
  63. {know_cli-0.3.1 → know_cli-0.3.3}/tests/test_week4.py +0 -0
  64. {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.1
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.1"
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
- from know.semantic_search import SemanticSearcher
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
- from know.semantic_search import SemanticSearcher
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,8 +0,0 @@
1
- """know - Living documentation generator for codebases."""
2
-
3
- __version__ = "0.3.0"
4
- __author__ = "Vic"
5
-
6
- from know.cli import main
7
-
8
- __all__ = ["main", "__version__"]
@@ -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