contextual-engine 0.1.0__py3-none-any.whl

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 (60) hide show
  1. contextual/__init__.py +18 -0
  2. contextual/__main__.py +11 -0
  3. contextual/cli.py +339 -0
  4. contextual/cli_docs.py +685 -0
  5. contextual/config.py +7 -0
  6. contextual/core/__init__.py +11 -0
  7. contextual/core/errors.py +470 -0
  8. contextual/core/models.py +590 -0
  9. contextual/docs/__init__.py +66 -0
  10. contextual/docs/chunker.py +550 -0
  11. contextual/docs/pipeline.py +513 -0
  12. contextual/docs/retrieval.py +654 -0
  13. contextual/docs/watcher.py +265 -0
  14. contextual/embedding/__init__.py +87 -0
  15. contextual/embedding/cache.py +455 -0
  16. contextual/embedding/embedder.py +414 -0
  17. contextual/embedding/helpers.py +252 -0
  18. contextual/git/__init__.py +22 -0
  19. contextual/git/blame.py +334 -0
  20. contextual/indexing/__init__.py +20 -0
  21. contextual/indexing/bug_sweep.py +119 -0
  22. contextual/indexing/chunker.py +691 -0
  23. contextual/indexing/embedder.py +271 -0
  24. contextual/indexing/file_watcher.py +154 -0
  25. contextual/indexing/incremental.py +260 -0
  26. contextual/indexing/index_writer.py +442 -0
  27. contextual/indexing/pipeline.py +438 -0
  28. contextual/indexing/processor.py +436 -0
  29. contextual/indexing/queries/readme.md +22 -0
  30. contextual/indexing/symbol_extractor.py +426 -0
  31. contextual/indexing/tokenizer.py +203 -0
  32. contextual/integrations/__init__.py +10 -0
  33. contextual/mcp/__init__.py +15 -0
  34. contextual/mcp/__main__.py +24 -0
  35. contextual/mcp/docs_tools.py +286 -0
  36. contextual/mcp/server.py +118 -0
  37. contextual/mcp/tools.py +443 -0
  38. contextual/observability/__init__.py +21 -0
  39. contextual/observability/logging.py +115 -0
  40. contextual/py.typed +0 -0
  41. contextual/retrieval/__init__.py +24 -0
  42. contextual/retrieval/context_assembler.py +372 -0
  43. contextual/retrieval/ranker.py +193 -0
  44. contextual/retrieval/search.py +548 -0
  45. contextual/security/__init__.py +52 -0
  46. contextual/security/paths.py +347 -0
  47. contextual/security/sanitize.py +349 -0
  48. contextual/security/workspace.py +348 -0
  49. contextual/storage/__init__.py +36 -0
  50. contextual/storage/fts_manager.py +273 -0
  51. contextual/storage/migration_v2.py +289 -0
  52. contextual/storage/migrations.py +316 -0
  53. contextual/storage/schema.py +210 -0
  54. contextual/storage/sqlite_pool.py +468 -0
  55. contextual/storage/vec0_manager.py +421 -0
  56. contextual_engine-0.1.0.dist-info/METADATA +297 -0
  57. contextual_engine-0.1.0.dist-info/RECORD +60 -0
  58. contextual_engine-0.1.0.dist-info/WHEEL +4 -0
  59. contextual_engine-0.1.0.dist-info/entry_points.txt +2 -0
  60. contextual_engine-0.1.0.dist-info/licenses/LICENSE +111 -0
contextual/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """Contextual: Temporal-first local AI context engine.
2
+
3
+ A tool-agnostic semantic code memory system that eliminates AI hallucinations
4
+ through bi-temporal fact tracking and deterministic retrieval.
5
+ """
6
+ from __future__ import annotations
7
+
8
+
9
+ __version__ = "0.1.0"
10
+ __author__ = "Contextual Team"
11
+ __license__ = "BSL-1.1"
12
+
13
+ # Public API exports will be added as modules are built
14
+ __all__ = [
15
+ "__author__",
16
+ "__license__",
17
+ "__version__",
18
+ ]
contextual/__main__.py ADDED
@@ -0,0 +1,11 @@
1
+ """Entry point for `python -m contextual` invocation.
2
+
3
+ Delegates to the Typer CLI defined in cli.py.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from contextual.cli import app
8
+
9
+
10
+ if __name__ == "__main__":
11
+ app()
contextual/cli.py ADDED
@@ -0,0 +1,339 @@
1
+ """CLI for Contextual - Temporal-first local code memory.
2
+
3
+ Main commands:
4
+ - init: Initialize configuration
5
+ - index: Index a code repository
6
+ - search: Search indexed code
7
+ - stats: Show index statistics
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Annotated, Optional
15
+
16
+ import typer
17
+ from rich.console import Console
18
+ from rich.table import Table
19
+
20
+ from contextual.embedding import embed_query_simple
21
+ from contextual.indexing.pipeline import IndexingPipeline
22
+ from contextual.observability.logging import get_logger
23
+ from contextual.retrieval.search import hybrid_search, SearchResult
24
+ from contextual.storage.sqlite_pool import SQLitePool
25
+
26
+
27
+ logger = get_logger(__name__)
28
+ console = Console()
29
+ app = typer.Typer(
30
+ name="contextual",
31
+ help="Temporal-first local code memory for AI tools",
32
+ add_completion=False,
33
+ )
34
+
35
+
36
+ @app.command()
37
+ def init(
38
+ path: Annotated[
39
+ Optional[Path],
40
+ typer.Argument(help="Repository path to initialize (default: current directory)"),
41
+ ] = None,
42
+ ) -> None:
43
+ """Initialize Contextual configuration for a repository.
44
+
45
+ Creates .contextual/ directory with database and config.
46
+ """
47
+ repo_path = (path or Path.cwd()).resolve()
48
+
49
+ if not repo_path.exists():
50
+ console.print(f"[red]Error:[/red] Path does not exist: {repo_path}")
51
+ raise typer.Exit(1)
52
+
53
+ # Create .contextual directory
54
+ contextual_dir = repo_path / ".contextual"
55
+ contextual_dir.mkdir(exist_ok=True)
56
+
57
+ # Initialize database
58
+ db_path = contextual_dir / "contextual.db"
59
+
60
+ console.print(f"[bold]Initializing Contextual in:[/bold] {repo_path}")
61
+
62
+ try:
63
+ # Create pool and run migrations
64
+ from contextual.storage.migrations import run_migrations
65
+
66
+ pool = SQLitePool(db_path)
67
+ with pool.writer() as conn:
68
+ run_migrations(conn)
69
+ pool.close()
70
+
71
+ console.print(f"[green]✓[/green] Created database: {db_path}")
72
+ console.print(f"[green]✓[/green] Initialization complete")
73
+ console.print(f"\n[dim]Next steps:[/dim]")
74
+ console.print(f" contextual index {repo_path}")
75
+ console.print(f" contextual search 'your query'")
76
+
77
+ except Exception as e:
78
+ console.print(f"[red]Error:[/red] Initialization failed: {e}")
79
+ logger.exception("Initialization failed")
80
+ raise typer.Exit(1)
81
+
82
+
83
+ @app.command()
84
+ def index(
85
+ path: Annotated[
86
+ Optional[Path],
87
+ typer.Argument(help="Repository path to index (default: current directory)"),
88
+ ] = None,
89
+ force: Annotated[
90
+ bool,
91
+ typer.Option("--force", "-f", help="Force re-index even if up-to-date"),
92
+ ] = False,
93
+ ) -> None:
94
+ """Index a code repository for semantic search.
95
+
96
+ Discovers source files, extracts symbols, generates embeddings,
97
+ and builds search index (BM25 + vector).
98
+ """
99
+ repo_path = (path or Path.cwd()).resolve()
100
+
101
+ if not repo_path.exists():
102
+ console.print(f"[red]Error:[/red] Path does not exist: {repo_path}")
103
+ raise typer.Exit(1)
104
+
105
+ # Check for .contextual directory
106
+ contextual_dir = repo_path / ".contextual"
107
+ if not contextual_dir.exists():
108
+ console.print(f"[yellow]Warning:[/yellow] Not initialized. Running init first...")
109
+ init(repo_path)
110
+
111
+ db_path = contextual_dir / "contextual.db"
112
+
113
+ console.print(f"[bold]Indexing:[/bold] {repo_path}")
114
+
115
+ try:
116
+ # Initialize pipeline
117
+ pool = SQLitePool(db_path)
118
+ pipeline = IndexingPipeline(repo_root=repo_path, db_pool=pool)
119
+
120
+ # Run indexing
121
+ with console.status("[bold green]Indexing files..."):
122
+ result = pipeline.index_repository()
123
+
124
+ # Display results
125
+ console.print(f"\n[green]✓[/green] Indexing complete")
126
+
127
+ # Results table
128
+ table = Table(show_header=True, header_style="bold cyan")
129
+ table.add_column("Metric", style="dim")
130
+ table.add_column("Value", justify="right")
131
+
132
+ table.add_row("Files indexed", str(result.get("files", 0)))
133
+ table.add_row("Chunks created", str(result.get("chunks", 0)))
134
+ table.add_row("Embeddings generated", str(result.get("embeddings", 0)))
135
+ table.add_row("Symbols extracted", str(result.get("symbols", 0)))
136
+ table.add_row("Duration", f"{result.get('duration_ms', 0) / 1000:.2f}s")
137
+
138
+ console.print(table)
139
+
140
+ pool.close()
141
+
142
+ except Exception as e:
143
+ console.print(f"[red]Error:[/red] Indexing failed: {e}")
144
+ logger.exception("Indexing failed")
145
+ raise typer.Exit(1)
146
+
147
+
148
+ @app.command()
149
+ def search(
150
+ query: Annotated[str, typer.Argument(help="Search query")],
151
+ path: Annotated[
152
+ Optional[Path],
153
+ typer.Option("--path", "-p", help="Repository path (default: current directory)"),
154
+ ] = None,
155
+ limit: Annotated[
156
+ int,
157
+ typer.Option("--limit", "-n", help="Number of results", min=1, max=50),
158
+ ] = 10,
159
+ language: Annotated[
160
+ Optional[str],
161
+ typer.Option("--language", "-l", help="Filter by language (py, ts, js, rs, go)"),
162
+ ] = None,
163
+ ) -> None:
164
+ """Search indexed code semantically.
165
+
166
+ Uses hybrid search (BM25 + vector embeddings) with RRF fusion.
167
+ """
168
+ repo_path = (path or Path.cwd()).resolve()
169
+ contextual_dir = repo_path / ".contextual"
170
+
171
+ if not contextual_dir.exists():
172
+ console.print(f"[red]Error:[/red] Repository not indexed. Run 'contextual index' first.")
173
+ raise typer.Exit(1)
174
+
175
+ db_path = contextual_dir / "contextual.db"
176
+
177
+ console.print(f"[bold]Searching:[/bold] {query}\n")
178
+
179
+ try:
180
+ # Initialize database
181
+ pool = SQLitePool(db_path)
182
+
183
+ # Embed query
184
+ query_embedding = embed_query_simple(query)
185
+
186
+ # Execute search
187
+ with pool.reader() as conn:
188
+ results: list[SearchResult] = hybrid_search(
189
+ conn=conn,
190
+ query=query,
191
+ query_embedding=query_embedding,
192
+ top_k=limit,
193
+ language=language,
194
+ )
195
+
196
+ if not results:
197
+ console.print("[yellow]No results found[/yellow]")
198
+ pool.close()
199
+ return
200
+
201
+ # Display results
202
+ for i, result in enumerate(results, 1):
203
+ console.print(f"[bold cyan]{i}. {result.path}[/bold cyan]")
204
+ console.print(f" Lines {result.start_line}-{result.end_line} | Score: {result.score:.3f}")
205
+
206
+ if result.symbol_name:
207
+ console.print(f" Symbol: [green]{result.symbol_name}[/green] ({result.chunk_type})")
208
+
209
+ # Show snippet
210
+ console.print(f" [dim]{result.snippet}[/dim]\n")
211
+
212
+ pool.close()
213
+
214
+ except Exception as e:
215
+ console.print(f"[red]Error:[/red] Search failed: {e}")
216
+ logger.exception("Search failed")
217
+ raise typer.Exit(1)
218
+
219
+
220
+ @app.command()
221
+ def stats(
222
+ path: Annotated[
223
+ Optional[Path],
224
+ typer.Argument(help="Repository path (default: current directory)"),
225
+ ] = None,
226
+ ) -> None:
227
+ """Show index statistics for a repository."""
228
+ repo_path = (path or Path.cwd()).resolve()
229
+ contextual_dir = repo_path / ".contextual"
230
+
231
+ if not contextual_dir.exists():
232
+ console.print(f"[red]Error:[/red] Repository not indexed. Run 'contextual index' first.")
233
+ raise typer.Exit(1)
234
+
235
+ db_path = contextual_dir / "contextual.db"
236
+
237
+ try:
238
+ pool = SQLitePool(db_path)
239
+
240
+ # Query statistics
241
+ with pool.reader() as conn:
242
+ # File count
243
+ cursor = conn.execute("SELECT COUNT(*) FROM files")
244
+ file_count = cursor.fetchone()[0]
245
+
246
+ # Chunk count
247
+ cursor = conn.execute("SELECT COUNT(*) FROM chunks")
248
+ chunk_count = cursor.fetchone()[0]
249
+
250
+ # Symbol count
251
+ cursor = conn.execute("SELECT COUNT(*) FROM code_symbols")
252
+ symbol_count = cursor.fetchone()[0]
253
+
254
+ # Language distribution
255
+ cursor = conn.execute("""
256
+ SELECT language, COUNT(*) as count
257
+ FROM files
258
+ GROUP BY language
259
+ ORDER BY count DESC
260
+ """)
261
+ languages = cursor.fetchall()
262
+
263
+ # Indexing state
264
+ cursor = conn.execute("""
265
+ SELECT last_full_index_at, total_files, total_chunks
266
+ FROM indexing_state
267
+ LIMIT 1
268
+ """)
269
+ state = cursor.fetchone()
270
+
271
+ # Display statistics
272
+ console.print(f"[bold]Statistics for:[/bold] {repo_path}\n")
273
+
274
+ table = Table(show_header=True, header_style="bold cyan")
275
+ table.add_column("Metric", style="dim")
276
+ table.add_column("Value", justify="right")
277
+
278
+ table.add_row("Files indexed", str(file_count))
279
+ table.add_row("Chunks created", str(chunk_count))
280
+ table.add_row("Symbols extracted", str(symbol_count))
281
+
282
+ if state:
283
+ table.add_row("Last indexed", str(state[0]))
284
+
285
+ console.print(table)
286
+
287
+ # Language distribution
288
+ if languages:
289
+ console.print("\n[bold]Languages:[/bold]")
290
+ lang_table = Table(show_header=False)
291
+ lang_table.add_column("Language", style="cyan")
292
+ lang_table.add_column("Files", justify="right")
293
+
294
+ for lang, count in languages:
295
+ lang_table.add_row(lang or "unknown", str(count))
296
+
297
+ console.print(lang_table)
298
+
299
+ # Database stats
300
+ db_stats = pool.get_db_stats()
301
+ console.print(f"\n[dim]Database size: {db_stats.get('db_size_bytes', 0) / 1024 / 1024:.2f} MB[/dim]")
302
+
303
+ pool.close()
304
+
305
+ except Exception as e:
306
+ console.print(f"[red]Error:[/red] Failed to get statistics: {e}")
307
+ logger.exception("Stats failed")
308
+ raise typer.Exit(1)
309
+
310
+
311
+ @app.command()
312
+ def version() -> None:
313
+ """Show Contextual version."""
314
+ try:
315
+ from contextual import __version__
316
+ console.print(f"Contextual version {__version__}")
317
+ except ImportError:
318
+ console.print("Contextual version 0.1.0")
319
+
320
+
321
+ def main() -> None:
322
+ """Entry point for CLI."""
323
+ try:
324
+ app()
325
+ except KeyboardInterrupt:
326
+ console.print("\n[yellow]Interrupted[/yellow]")
327
+ sys.exit(130)
328
+ except Exception as e:
329
+ console.print(f"[red]Fatal error:[/red] {e}")
330
+ logger.exception("Fatal error")
331
+ sys.exit(1)
332
+
333
+ from contextual.cli_docs import docs_app, setup_app
334
+ app.add_typer(docs_app, name="docs", help="Index and search documentation.")
335
+ app.add_typer(setup_app, name="setup", help="Configure Contextual in your IDE/AI client.")
336
+
337
+
338
+ if __name__ == "__main__":
339
+ main()