tenets 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 (139) hide show
  1. tenets/__init__.py +979 -0
  2. tenets/__main__.py +13 -0
  3. tenets/cli/__init__.py +27 -0
  4. tenets/cli/__main__.py +13 -0
  5. tenets/cli/app.py +243 -0
  6. tenets/cli/commands/__init__.py +14 -0
  7. tenets/cli/commands/_utils.py +16 -0
  8. tenets/cli/commands/chronicle.py +495 -0
  9. tenets/cli/commands/config.py +667 -0
  10. tenets/cli/commands/distill.py +596 -0
  11. tenets/cli/commands/examine.py +642 -0
  12. tenets/cli/commands/instill.py +776 -0
  13. tenets/cli/commands/momentum.py +763 -0
  14. tenets/cli/commands/rank.py +1337 -0
  15. tenets/cli/commands/session.py +297 -0
  16. tenets/cli/commands/system_instruction.py +476 -0
  17. tenets/cli/commands/tenet.py +784 -0
  18. tenets/cli/commands/viz.py +1144 -0
  19. tenets/config.py +1685 -0
  20. tenets/core/__init__.py +47 -0
  21. tenets/core/analysis/__init__.py +11 -0
  22. tenets/core/analysis/analyzer.py +1307 -0
  23. tenets/core/analysis/base.py +201 -0
  24. tenets/core/analysis/implementations/__init__.py +65 -0
  25. tenets/core/analysis/implementations/cpp_analyzer.py +1097 -0
  26. tenets/core/analysis/implementations/csharp_analyzer.py +1533 -0
  27. tenets/core/analysis/implementations/css_analyzer.py +1305 -0
  28. tenets/core/analysis/implementations/dart_analyzer.py +1348 -0
  29. tenets/core/analysis/implementations/gdscript_analyzer.py +946 -0
  30. tenets/core/analysis/implementations/generic_analyzer.py +1791 -0
  31. tenets/core/analysis/implementations/go_analyzer.py +1022 -0
  32. tenets/core/analysis/implementations/html_analyzer.py +910 -0
  33. tenets/core/analysis/implementations/java_analyzer.py +1017 -0
  34. tenets/core/analysis/implementations/javascript_analyzer.py +1021 -0
  35. tenets/core/analysis/implementations/kotlin_analyzer.py +1126 -0
  36. tenets/core/analysis/implementations/php_analyzer.py +1259 -0
  37. tenets/core/analysis/implementations/python_analyzer.py +1006 -0
  38. tenets/core/analysis/implementations/ruby_analyzer.py +1138 -0
  39. tenets/core/analysis/implementations/rust_analyzer.py +1185 -0
  40. tenets/core/analysis/implementations/scala_analyzer.py +1211 -0
  41. tenets/core/analysis/implementations/swift_analyzer.py +1247 -0
  42. tenets/core/analysis/project_detector.py +339 -0
  43. tenets/core/distiller/__init__.py +21 -0
  44. tenets/core/distiller/aggregator.py +410 -0
  45. tenets/core/distiller/distiller.py +468 -0
  46. tenets/core/distiller/formatter.py +1485 -0
  47. tenets/core/distiller/optimizer.py +322 -0
  48. tenets/core/distiller/transform.py +205 -0
  49. tenets/core/examiner/__init__.py +282 -0
  50. tenets/core/examiner/complexity.py +1148 -0
  51. tenets/core/examiner/examiner.py +767 -0
  52. tenets/core/examiner/hotspots.py +1914 -0
  53. tenets/core/examiner/metrics.py +758 -0
  54. tenets/core/examiner/ownership.py +1003 -0
  55. tenets/core/git/__init__.py +501 -0
  56. tenets/core/git/analyzer.py +517 -0
  57. tenets/core/git/blame.py +977 -0
  58. tenets/core/git/chronicle.py +1111 -0
  59. tenets/core/git/stats.py +1132 -0
  60. tenets/core/instiller/__init__.py +18 -0
  61. tenets/core/instiller/injector.py +507 -0
  62. tenets/core/instiller/instiller.py +1419 -0
  63. tenets/core/instiller/manager.py +649 -0
  64. tenets/core/momentum/__init__.py +448 -0
  65. tenets/core/momentum/metrics.py +833 -0
  66. tenets/core/momentum/tracker.py +1569 -0
  67. tenets/core/nlp/__init__.py +165 -0
  68. tenets/core/nlp/bm25.py +572 -0
  69. tenets/core/nlp/cache.py +194 -0
  70. tenets/core/nlp/embeddings.py +284 -0
  71. tenets/core/nlp/keyword_extractor.py +1107 -0
  72. tenets/core/nlp/ml_utils.py +365 -0
  73. tenets/core/nlp/programming_patterns.py +493 -0
  74. tenets/core/nlp/similarity.py +318 -0
  75. tenets/core/nlp/stopwords.py +235 -0
  76. tenets/core/nlp/tfidf.py +170 -0
  77. tenets/core/nlp/tokenizer.py +201 -0
  78. tenets/core/prompt/__init__.py +354 -0
  79. tenets/core/prompt/cache.py +494 -0
  80. tenets/core/prompt/entity_recognizer.py +950 -0
  81. tenets/core/prompt/external_sources.py +30 -0
  82. tenets/core/prompt/intent_detector.py +941 -0
  83. tenets/core/prompt/normalizer.py +111 -0
  84. tenets/core/prompt/parser.py +1584 -0
  85. tenets/core/prompt/temporal_parser.py +1014 -0
  86. tenets/core/ranking/__init__.py +304 -0
  87. tenets/core/ranking/factors.py +525 -0
  88. tenets/core/ranking/ranker.py +881 -0
  89. tenets/core/ranking/strategies.py +958 -0
  90. tenets/core/reporting/__init__.py +653 -0
  91. tenets/core/reporting/generator.py +1506 -0
  92. tenets/core/reporting/html_reporter.py +1419 -0
  93. tenets/core/reporting/markdown_reporter.py +726 -0
  94. tenets/core/reporting/visualizer.py +1056 -0
  95. tenets/core/session/__init__.py +1 -0
  96. tenets/core/session/session.py +99 -0
  97. tenets/core/summarizer/__init__.py +409 -0
  98. tenets/core/summarizer/llm.py +472 -0
  99. tenets/core/summarizer/strategies.py +837 -0
  100. tenets/core/summarizer/summarizer.py +1691 -0
  101. tenets/data/pattterns/entity_patterns.json +1317 -0
  102. tenets/data/pattterns/external_patterns.json +673 -0
  103. tenets/data/pattterns/intent_patterns.json +378 -0
  104. tenets/data/pattterns/programming_patterns.json +417 -0
  105. tenets/data/pattterns/temporal_patterns.json +751 -0
  106. tenets/data/stopwords/minimal.txt +48 -0
  107. tenets/data/stopwords/prompt_aggressive.txt +369 -0
  108. tenets/models/__init__.py +85 -0
  109. tenets/models/analysis.py +1100 -0
  110. tenets/models/context.py +490 -0
  111. tenets/models/llm.py +143 -0
  112. tenets/models/summary.py +436 -0
  113. tenets/models/tenet.py +340 -0
  114. tenets/storage/__init__.py +19 -0
  115. tenets/storage/cache.py +436 -0
  116. tenets/storage/session_db.py +284 -0
  117. tenets/storage/sqlite.py +131 -0
  118. tenets/utils/__init__.py +16 -0
  119. tenets/utils/external_sources.py +895 -0
  120. tenets/utils/logger.py +177 -0
  121. tenets/utils/multiprocessing.py +147 -0
  122. tenets/utils/scanner.py +431 -0
  123. tenets/utils/timing.py +650 -0
  124. tenets/utils/tokens.py +296 -0
  125. tenets/viz/__init__.py +686 -0
  126. tenets/viz/base.py +737 -0
  127. tenets/viz/complexity.py +442 -0
  128. tenets/viz/contributors.py +438 -0
  129. tenets/viz/coupling.py +459 -0
  130. tenets/viz/dependencies.py +528 -0
  131. tenets/viz/displays.py +423 -0
  132. tenets/viz/graph_generator.py +929 -0
  133. tenets/viz/hotspots.py +414 -0
  134. tenets/viz/momentum.py +439 -0
  135. tenets-0.1.0.dist-info/METADATA +414 -0
  136. tenets-0.1.0.dist-info/RECORD +139 -0
  137. tenets-0.1.0.dist-info/WHEEL +4 -0
  138. tenets-0.1.0.dist-info/entry_points.txt +2 -0
  139. tenets-0.1.0.dist-info/licenses/LICENSE +21 -0
tenets/__init__.py ADDED
@@ -0,0 +1,979 @@
1
+ """Tenets - Context that feeds your prompts.
2
+
3
+ Tenets is a code intelligence platform that analyzes codebases locally to surface
4
+ relevant files, track development velocity, and build optimal context for both
5
+ human understanding and AI pair programming - all without making any LLM API calls.
6
+
7
+ This package provides:
8
+
9
+ Example:
10
+ Basic usage for context extraction:
11
+
12
+ >>> from tenets import Tenets
13
+ >>> ten = Tenets()
14
+ >>> result = ten.distill("implement OAuth2 authentication")
15
+ >>> print(result.context)
16
+
17
+ With tenet system:
18
+
19
+ >>> ten.add_tenet("Always use type hints in Python", priority="high")
20
+ >>> ten.instill_tenets()
21
+ >>> result = ten.distill("add user model") # Context now includes tenets
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ __version__ = "0.1.0"
27
+ __author__ = "Johnny Dunn"
28
+ __license__ = "MIT"
29
+
30
+ import os
31
+ import sys
32
+ from pathlib import Path
33
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
34
+
35
+ # Check Python version
36
+ if sys.version_info < (3, 9):
37
+ raise RuntimeError("Tenets requires Python 3.9 or higher")
38
+
39
+ # Keep runtime imports lightweight. Only import heavy modules lazily.
40
+ from tenets.config import TenetsConfig
41
+ from tenets.models.context import ContextResult # re-export for public API/tests
42
+ from tenets.models.tenet import Priority, Tenet, TenetCategory # re-export for public API/tests
43
+ from tenets.utils.logger import get_logger
44
+
45
+ # Lazy imports using standard Python 3.7+ __getattr__ (PEP 562)
46
+ # This allows tests to patch at package level and improves import performance
47
+ _LAZY_IMPORTS = {
48
+ "Distiller": "tenets.core.distiller.Distiller",
49
+ "Instiller": "tenets.core.instiller.Instiller",
50
+ "CodeAnalyzer": "tenets.core.analysis.analyzer.CodeAnalyzer",
51
+ "TenetManager": "tenets.core.instiller.manager.TenetManager",
52
+ "ContextResult": "tenets.models.context.ContextResult",
53
+ "Priority": "tenets.models.tenet.Priority",
54
+ "Tenet": "tenets.models.tenet.Tenet",
55
+ "TenetCategory": "tenets.models.tenet.TenetCategory",
56
+ }
57
+
58
+
59
+ def __getattr__(name):
60
+ """Lazy import heavy components on first access.
61
+
62
+ This is the standard Python 3.7+ way to implement lazy imports (PEP 562).
63
+ It preserves type hints, works with IDEs, and maintains proper class identity.
64
+ """
65
+ if name in _LAZY_IMPORTS:
66
+ import importlib
67
+
68
+ module_path, attr_name = _LAZY_IMPORTS[name].rsplit(".", 1)
69
+ module = importlib.import_module(module_path)
70
+ attr = getattr(module, attr_name)
71
+ # Cache for future access
72
+ globals()[name] = attr
73
+ return attr
74
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
75
+
76
+
77
+ # Type-checking only imports (no runtime side-effects)
78
+ if TYPE_CHECKING: # pragma: no cover - used only for typing
79
+ from tenets.core.analysis.analyzer import CodeAnalyzer
80
+ from tenets.core.distiller import Distiller
81
+ from tenets.core.instiller import Instiller
82
+ from tenets.core.instiller.manager import TenetManager
83
+ from tenets.models.context import ContextResult
84
+ from tenets.models.tenet import Priority, Tenet, TenetCategory
85
+
86
+
87
+ class Tenets:
88
+ """Main API interface for the Tenets system.
89
+
90
+ This is the primary class that users interact with to access all Tenets
91
+ functionality. It coordinates between the various subsystems (distiller,
92
+ instiller, analyzer, etc.) to provide a unified interface.
93
+
94
+ The Tenets class can be used both programmatically through Python and via
95
+ the CLI. It maintains configuration, manages sessions, and orchestrates
96
+ the various analysis and context generation operations.
97
+
98
+ Attributes:
99
+ config: TenetsConfig instance containing all configuration
100
+ distiller: Distiller instance for context extraction
101
+ instiller: Instiller instance for tenet management
102
+ tenet_manager: Direct access to TenetManager for advanced operations
103
+ logger: Logger instance for this class
104
+ _session: Current session name if any
105
+ _cache: Internal cache for results
106
+
107
+ Example:
108
+ >>> from tenets import Tenets
109
+ >>> from pathlib import Path
110
+ >>>
111
+ >>> # Initialize with default config
112
+ >>> ten = Tenets()
113
+ >>>
114
+ >>> # Or with custom config
115
+ >>> from tenets.config import TenetsConfig
116
+ >>> config = TenetsConfig(max_tokens=150000, ranking_algorithm="thorough")
117
+ >>> ten = Tenets(config=config)
118
+ >>>
119
+ >>> # Extract context (uses default session automatically)
120
+ >>> result = ten.distill("implement user authentication")
121
+ >>> print(f"Generated {result.token_count} tokens of context")
122
+ >>>
123
+ >>> # Generate HTML report
124
+ >>> result = ten.distill("review API endpoints", format="html")
125
+ >>> Path("api-review.html").write_text(result.context)
126
+ >>>
127
+ >>> # Add and apply tenets
128
+ >>> ten.add_tenet("Use dependency injection", priority="high")
129
+ >>> ten.add_tenet("Follow RESTful conventions", category="architecture")
130
+ >>> ten.instill_tenets()
131
+ >>>
132
+ >>> # Pin critical files for priority inclusion
133
+ >>> ten.pin_file("src/core/auth.py")
134
+ >>> ten.pin_folder("src/api/endpoints")
135
+ >>>
136
+ >>> # Work with named sessions
137
+ >>> result = ten.distill(
138
+ ... "implement OAuth2",
139
+ ... session_name="oauth-feature",
140
+ ... mode="thorough"
141
+ ... )
142
+ """
143
+
144
+ def __init__(self, config: Optional[Union[TenetsConfig, dict[str, Any], Path]] = None):
145
+ """Initialize Tenets with configuration.
146
+
147
+ Args:
148
+ config: Can be:
149
+ - TenetsConfig instance
150
+ - Dictionary of configuration values
151
+ - Path to configuration file
152
+ - None (uses default configuration)
153
+
154
+ Raises:
155
+ ValueError: If config format is invalid
156
+ FileNotFoundError: If config file path doesn't exist
157
+ """
158
+ # Handle different config input types
159
+ if config is None:
160
+ self.config = TenetsConfig()
161
+ elif isinstance(config, TenetsConfig):
162
+ self.config = config
163
+ elif isinstance(config, dict):
164
+ # Map common top-level aliases into nested config structure
165
+ cfg = TenetsConfig()
166
+ # Known top-level shortcuts used in docs/tests
167
+ if "max_tokens" in config:
168
+ cfg.max_tokens = int(config["max_tokens"]) # type: ignore[arg-type]
169
+ if "debug" in config:
170
+ cfg.debug = bool(config["debug"]) # type: ignore[arg-type]
171
+ if "ranking_algorithm" in config:
172
+ cfg.ranking.algorithm = str(config["ranking_algorithm"]) # type: ignore[arg-type]
173
+ # Apply any nested sections if provided
174
+ if "scanner" in config and isinstance(config["scanner"], dict):
175
+ cfg.scanner = type(cfg.scanner)(**config["scanner"]) # type: ignore[call-arg]
176
+ if "ranking" in config and isinstance(config["ranking"], dict):
177
+ cfg.ranking = type(cfg.ranking)(**config["ranking"]) # type: ignore[call-arg]
178
+ if "tenet" in config and isinstance(config["tenet"], dict):
179
+ cfg.tenet = type(cfg.tenet)(**config["tenet"]) # type: ignore[call-arg]
180
+ if "cache" in config and isinstance(config["cache"], dict):
181
+ cfg.cache = type(cfg.cache)(**config["cache"]) # type: ignore[call-arg]
182
+ if "output" in config and isinstance(config["output"], dict):
183
+ cfg.output = type(cfg.output)(**config["output"]) # type: ignore[call-arg]
184
+ if "git" in config and isinstance(config["git"], dict):
185
+ cfg.git = type(cfg.git)(**config["git"]) # type: ignore[call-arg]
186
+ # Any other keys go to custom
187
+ for k, v in config.items():
188
+ if k not in {
189
+ "max_tokens",
190
+ "debug",
191
+ "ranking_algorithm",
192
+ "scanner",
193
+ "ranking",
194
+ "tenet",
195
+ "cache",
196
+ "output",
197
+ "git",
198
+ }:
199
+ cfg.custom[k] = v
200
+ self.config = cfg
201
+ elif isinstance(config, (str, Path)):
202
+ config_path = Path(config)
203
+ if not config_path.exists():
204
+ raise FileNotFoundError(f"Config file not found: {config_path}")
205
+ self.config = TenetsConfig(config_file=config_path)
206
+ else:
207
+ raise ValueError(f"Invalid config type: {type(config)}")
208
+
209
+ # Initialize logger (import locally to avoid circular import)
210
+ from tenets.utils.logger import get_logger
211
+
212
+ self.logger = get_logger(__name__)
213
+ self.logger.info(f"Initializing Tenets v{__version__}")
214
+
215
+ # Lazy-load core components to improve import performance
216
+ self._distiller = None
217
+ self._instiller = None
218
+ self._tenet_manager = None
219
+
220
+ # Session management
221
+ self._session = None
222
+ self._session_data = {}
223
+
224
+ # Internal cache
225
+ self._cache = {}
226
+
227
+ self.logger.info("Tenets initialization complete")
228
+
229
+ @property
230
+ def distiller(self):
231
+ """Lazy load distiller when needed."""
232
+ if self._distiller is None:
233
+ # Import locally to trigger lazy loading
234
+ from tenets.core.distiller import Distiller
235
+
236
+ self._distiller = Distiller(self.config)
237
+ return self._distiller
238
+
239
+ @property
240
+ def instiller(self):
241
+ """Lazy load instiller when needed."""
242
+ if self._instiller is None:
243
+ # Import locally to trigger lazy loading
244
+ from tenets.core.instiller import Instiller
245
+
246
+ self._instiller = Instiller(self.config)
247
+ return self._instiller
248
+
249
+ @property
250
+ def tenet_manager(self):
251
+ """Lazy load tenet manager when needed."""
252
+ if self._tenet_manager is None:
253
+ if self._instiller is None:
254
+ # Import locally to trigger lazy loading
255
+ from tenets.core.instiller import Instiller
256
+
257
+ self._instiller = Instiller(self.config)
258
+ self._tenet_manager = self._instiller.manager
259
+ return self._tenet_manager
260
+
261
+ # ============= Core Distillation Methods =============
262
+
263
+ def distill(
264
+ self,
265
+ prompt: str,
266
+ files: Optional[Union[str, Path, list[Path]]] = None,
267
+ *, # Force keyword-only arguments
268
+ format: str = "markdown",
269
+ model: Optional[str] = None,
270
+ max_tokens: Optional[int] = None,
271
+ mode: str = "balanced",
272
+ include_git: bool = True,
273
+ session_name: Optional[str] = None,
274
+ include_patterns: Optional[list[str]] = None,
275
+ exclude_patterns: Optional[list[str]] = None,
276
+ apply_tenets: Optional[bool] = None,
277
+ full: bool = False,
278
+ condense: bool = False,
279
+ remove_comments: bool = False,
280
+ include_tests: Optional[bool] = None,
281
+ docstring_weight: Optional[float] = None,
282
+ summarize_imports: bool = True,
283
+ ) -> ContextResult:
284
+ """Distill relevant context from codebase based on prompt.
285
+
286
+ This is the main method for extracting context. It analyzes your codebase,
287
+ finds relevant files, ranks them by importance, and aggregates them into
288
+ an optimized context that fits within token limits.
289
+
290
+ Args:
291
+ prompt: Your query or task description. Can be plain text or a URL
292
+ to a GitHub issue, JIRA ticket, etc.
293
+ files: Paths to analyze. Can be a single path, list of paths, or None
294
+ to use current directory
295
+ format: Output format - 'markdown', 'xml' (Claude), 'json', or 'html' (interactive report)
296
+ model: Target LLM model for token counting (e.g., 'gpt-4o', 'claude-3-opus')
297
+ max_tokens: Maximum tokens for context (overrides model default)
298
+ mode: Analysis mode - 'fast', 'balanced', or 'thorough'
299
+ include_git: Whether to include git context (commits, contributors, etc.)
300
+ session_name: Session name for stateful context building
301
+ include_patterns: File patterns to include (e.g., ['*.py', '*.js'])
302
+ exclude_patterns: File patterns to exclude (e.g., ['test_*', '*.backup'])
303
+ apply_tenets: Whether to apply tenets (None = use config default)
304
+
305
+ Returns:
306
+ ContextResult containing the generated context, metadata, and statistics.
307
+ The metadata field includes timing information when available:
308
+ metadata['timing'] = {
309
+ 'duration': 2.34, # seconds
310
+ 'formatted_duration': '2.34s', # Human-readable duration string
311
+ 'start_datetime': '2024-01-15T10:30:45',
312
+ 'end_datetime': '2024-01-15T10:30:47'
313
+ }
314
+
315
+ Raises:
316
+ ValueError: If prompt is empty or invalid
317
+ FileNotFoundError: If specified files don't exist
318
+
319
+ Example:
320
+ >>> # Basic usage (uses default session automatically)
321
+ >>> result = tenets.distill("implement OAuth2 authentication")
322
+ >>> print(result.context[:100]) # First 100 chars of context
323
+ >>>
324
+ >>> # With specific files and options
325
+ >>> result = tenets.distill(
326
+ ... "add caching layer",
327
+ ... files="./src",
328
+ ... mode="thorough",
329
+ ... max_tokens=50000,
330
+ ... include_patterns=["*.py"],
331
+ ... exclude_patterns=["test_*.py"]
332
+ ... )
333
+ >>>
334
+ >>> # Generate HTML report
335
+ >>> result = tenets.distill(
336
+ ... "analyze authentication flow",
337
+ ... format="html"
338
+ ... )
339
+ >>> Path("report.html").write_text(result.context)
340
+ >>>
341
+ >>> # With session management
342
+ >>> result = tenets.distill(
343
+ ... "implement validation",
344
+ ... session_name="validation-feature"
345
+ ... )
346
+ >>>
347
+ >>> # From GitHub issue
348
+ >>> result = tenets.distill("https://github.com/org/repo/issues/123")
349
+ >>>
350
+ >>> # Access timing information
351
+ >>> result = tenets.distill("analyze performance")
352
+ >>> if 'timing' in result.metadata:
353
+ ... print(f"Analysis took {result.metadata['timing']['formatted_duration']}")
354
+ ... # Output: "Analysis took 2.34s"
355
+ """
356
+ if not prompt:
357
+ raise ValueError("Prompt cannot be empty")
358
+
359
+ self.logger.info(f"Distilling context for: {prompt[:100]}...")
360
+
361
+ # Use session if specified or default session
362
+ session = session_name or self._session
363
+
364
+ # Run distillation
365
+ pinned_files = []
366
+ try:
367
+ pf_map = self.config.custom.get("pinned_files", {})
368
+ if session and pf_map and session in pf_map:
369
+ pinned_files = [Path(p) for p in pf_map[session] if Path(p).exists()]
370
+ # Supplement from session DB metadata
371
+ if session and not pinned_files:
372
+ try:
373
+ from tenets.storage.session_db import SessionDB
374
+
375
+ sdb = SessionDB(self.config)
376
+ rec = sdb.get_session(session)
377
+ if rec and rec.metadata.get("pinned_files"):
378
+ pinned_files = [
379
+ Path(p)
380
+ for p in rec.metadata.get("pinned_files", [])
381
+ if Path(p).exists()
382
+ ]
383
+ except Exception: # pragma: no cover
384
+ pass
385
+ except Exception: # pragma: no cover
386
+ pinned_files = []
387
+ result = self.distiller.distill(
388
+ prompt=prompt,
389
+ paths=files,
390
+ format=format,
391
+ model=model,
392
+ max_tokens=max_tokens,
393
+ mode=mode,
394
+ include_git=include_git,
395
+ session_name=session,
396
+ include_patterns=include_patterns,
397
+ exclude_patterns=exclude_patterns,
398
+ full=full,
399
+ condense=condense,
400
+ remove_comments=remove_comments,
401
+ pinned_files=pinned_files or None,
402
+ include_tests=include_tests,
403
+ docstring_weight=docstring_weight,
404
+ summarize_imports=summarize_imports,
405
+ )
406
+
407
+ # Inject system instruction if configured (skip for HTML reports meant for humans)
408
+ if format.lower() != "html":
409
+ try:
410
+ modified, meta = self.instiller.inject_system_instruction(
411
+ result.context, format=result.format, session=session
412
+ )
413
+ if meta.get("system_instruction_injected"):
414
+ result = ContextResult(
415
+ files=result.files,
416
+ context=modified,
417
+ format=result.format,
418
+ metadata={**result.metadata, "system_instruction": meta},
419
+ )
420
+ except Exception:
421
+ # Best-effort; don't fail distill if injection fails
422
+ pass
423
+
424
+ # Apply tenets if configured
425
+ should_apply_tenets = (
426
+ apply_tenets if apply_tenets is not None else self.config.auto_instill_tenets
427
+ )
428
+
429
+ pending = None
430
+ if should_apply_tenets:
431
+ try:
432
+ pending = self.tenet_manager.get_pending_tenets(session)
433
+ except Exception:
434
+ pending = []
435
+
436
+ def _has_real_pending(items) -> bool:
437
+ try:
438
+ return isinstance(items, list) and len(items) > 0
439
+ except Exception:
440
+ return False
441
+
442
+ if should_apply_tenets and _has_real_pending(pending):
443
+ self.logger.info("Applying tenets to context")
444
+ result = self.instiller.instill(
445
+ context=result,
446
+ session=session,
447
+ max_tenets=self.config.max_tenets_per_context,
448
+ inject_system_instruction=False, # Already injected above
449
+ )
450
+
451
+ # Cache result
452
+ cache_key = f"{prompt[:50]}_{session or 'global'}"
453
+ self._cache[cache_key] = result
454
+
455
+ return result
456
+
457
+ def rank_files(
458
+ self,
459
+ prompt: str,
460
+ paths: Optional[Union[str, Path, List[Path]]] = None,
461
+ *, # Force keyword-only arguments
462
+ mode: str = "balanced",
463
+ include_patterns: Optional[List[str]] = None,
464
+ exclude_patterns: Optional[List[str]] = None,
465
+ include_tests: Optional[bool] = None,
466
+ exclude_tests: bool = False,
467
+ explain: bool = False,
468
+ ) -> RankResult:
469
+ """Rank files by relevance without generating full context.
470
+
471
+ This method uses the same sophisticated ranking pipeline as distill()
472
+ but returns only the ranked files without aggregating content.
473
+ Perfect for understanding which files are relevant or for automation.
474
+
475
+ Args:
476
+ prompt: Your query or task description
477
+ paths: Paths to analyze (default: current directory)
478
+ mode: Analysis mode - 'fast', 'balanced', or 'thorough'
479
+ include_patterns: File patterns to include
480
+ exclude_patterns: File patterns to exclude
481
+ include_tests: Whether to include test files
482
+ exclude_tests: Whether to exclude test files
483
+ explain: Whether to include ranking factor explanations
484
+
485
+ Returns:
486
+ RankResult containing the ranked files and metadata
487
+
488
+ Example:
489
+ >>> result = ten.rank_files("fix summarizing truncation bug")
490
+ >>> for file in result.files:
491
+ ... print(f"{file.path}: {file.relevance_score:.3f}")
492
+ """
493
+ # Use the same pipeline as distill but stop at ranking
494
+
495
+ # 1. Parse and understand the prompt
496
+ prompt_context = self.distiller._parse_prompt(prompt)
497
+
498
+ # Override test inclusion if explicitly specified
499
+ if include_tests is not None:
500
+ prompt_context.include_tests = include_tests
501
+ elif exclude_tests:
502
+ prompt_context.include_tests = False
503
+
504
+ # 2. Determine paths to analyze
505
+ paths = self.distiller._normalize_paths(paths)
506
+
507
+ # 3. Discover relevant files
508
+ files = self.distiller._discover_files(
509
+ paths=paths,
510
+ prompt_context=prompt_context,
511
+ include_patterns=include_patterns,
512
+ exclude_patterns=exclude_patterns,
513
+ )
514
+
515
+ # 4. Analyze files for structure and content
516
+ analyzed_files = self.distiller._analyze_files(
517
+ files=files, mode=mode, prompt_context=prompt_context
518
+ )
519
+
520
+ # 5. Rank files by relevance (this is what we want!)
521
+ ranked_files = self.distiller._rank_files(
522
+ files=analyzed_files, prompt_context=prompt_context, mode=mode
523
+ )
524
+
525
+ # Create result object
526
+ from collections import namedtuple
527
+
528
+ RankResult = namedtuple("RankResult", ["files", "prompt_context", "mode", "total_scanned"])
529
+
530
+ return RankResult(
531
+ files=ranked_files, prompt_context=prompt_context, mode=mode, total_scanned=len(files)
532
+ )
533
+
534
+ # ============= Tenet Management Methods =============
535
+
536
+ def add_tenet(
537
+ self,
538
+ content: str,
539
+ priority: Union[str, Priority] = "medium",
540
+ category: Optional[Union[str, TenetCategory]] = None,
541
+ session: Optional[str] = None,
542
+ author: Optional[str] = None,
543
+ ) -> Tenet:
544
+ """Add a new guiding principle (tenet).
545
+
546
+ Tenets are persistent instructions that get strategically injected into
547
+ generated context to maintain consistency across AI interactions. They
548
+ help combat context drift and ensure important principles are followed.
549
+
550
+ Args:
551
+ content: The guiding principle text
552
+ priority: Priority level - 'low', 'medium', 'high', or 'critical'
553
+ category: Optional category - 'architecture', 'security', 'style',
554
+ 'performance', 'testing', 'documentation', etc.
555
+ session: Optional session to bind this tenet to
556
+ author: Optional author identifier
557
+
558
+ Returns:
559
+ The created Tenet object
560
+
561
+ Example:
562
+ >>> # Add a high-priority security tenet
563
+ >>> tenet = ten.add_tenet(
564
+ ... "Always validate and sanitize user input",
565
+ ... priority="high",
566
+ ... category="security"
567
+ ... )
568
+ >>>
569
+ >>> # Add a session-specific tenet
570
+ >>> ten.add_tenet(
571
+ ... "Use async/await for all I/O operations",
572
+ ... session="async-refactor"
573
+ ... )
574
+ """
575
+ return self.tenet_manager.add_tenet(
576
+ content=content,
577
+ priority=priority,
578
+ category=category,
579
+ session=session or self._session,
580
+ author=author,
581
+ )
582
+
583
+ def instill_tenets(self, session: Optional[str] = None, force: bool = False) -> Dict[str, Any]:
584
+ """Instill pending tenets.
585
+
586
+ This marks tenets as active and ready to be injected into future contexts.
587
+ By default, only pending tenets are instilled, but you can force
588
+ re-instillation of all tenets.
589
+
590
+ Args:
591
+ session: Optional session to instill tenets for
592
+ force: If True, re-instill even already instilled tenets
593
+
594
+ Returns:
595
+ Dictionary with instillation results including count and tenets
596
+
597
+ Example:
598
+ >>> # Instill all pending tenets
599
+ >>> result = ten.instill_tenets()
600
+ >>> print(f"Instilled {result['count']} tenets")
601
+ >>>
602
+ >>> # Force re-instillation
603
+ >>> ten.instill_tenets(force=True)
604
+ """
605
+ return self.tenet_manager.instill_tenets(session=session or self._session, force=force)
606
+
607
+ # ============= Session Pinning Utilities =============
608
+
609
+ def _ensure_session(self, session: Optional[str]) -> str:
610
+ """Ensure a session exists and return its name."""
611
+ name = session or self._session or "default"
612
+ if not self._session:
613
+ self._session = name
614
+ # Create in session manager (if available)
615
+ try:
616
+ from tenets.core.session.session import SessionManager # type: ignore
617
+
618
+ # Lazy create a manager if not present (some tests may not use it directly)
619
+ except Exception: # pragma: no cover - defensive
620
+ return name
621
+ return name
622
+
623
+ def add_file_to_session(
624
+ self, file_path: Union[str, Path], session: Optional[str] = None
625
+ ) -> bool:
626
+ """Pin a single file into a session so it is prioritized in future distill calls.
627
+
628
+ Args:
629
+ file_path: Path to file
630
+ session: Optional session name
631
+ Returns:
632
+ True if file pinned, False otherwise
633
+ """
634
+ path = Path(file_path)
635
+ if not path.exists() or not path.is_file():
636
+ return False
637
+ sess_name = session or self._session or "default"
638
+ # Attach to in-memory session context held by session manager if available
639
+ try:
640
+ from tenets.core.session.session import SessionManager # local import
641
+
642
+ # There may or may not be a global session manager; instantiate lightweight if needed
643
+ except Exception: # pragma: no cover
644
+ pass
645
+ # For now store pinned files in config.custom for persistence stub
646
+ if "pinned_files" not in self.config.custom:
647
+ self.config.custom["pinned_files"] = {}
648
+ self.config.custom["pinned_files"].setdefault(sess_name, set())
649
+ resolved = str(path.resolve())
650
+ self.config.custom["pinned_files"][sess_name].add(resolved)
651
+ # Persist in session DB metadata if available
652
+ try:
653
+ from tenets.storage.session_db import SessionDB # local import
654
+
655
+ sdb = SessionDB(self.config)
656
+ # Read current metadata and merge
657
+ rec = sdb.get_session(sess_name)
658
+ existing = rec.metadata.get("pinned_files") if rec else []
659
+ if isinstance(existing, list):
660
+ if resolved not in existing:
661
+ existing.append(resolved)
662
+ else:
663
+ existing = [resolved]
664
+ sdb.update_session_metadata(sess_name, {"pinned_files": existing})
665
+ except Exception: # pragma: no cover - best effort
666
+ pass
667
+ return True
668
+
669
+ def add_folder_to_session(
670
+ self,
671
+ folder_path: Union[str, Path],
672
+ session: Optional[str] = None,
673
+ include_patterns: Optional[list[str]] = None,
674
+ exclude_patterns: Optional[list[str]] = None,
675
+ respect_gitignore: bool = True,
676
+ recursive: bool = True,
677
+ ) -> int:
678
+ """Pin all files in a folder (optionally filtered) into a session.
679
+
680
+ Args:
681
+ folder_path: Directory to scan
682
+ session: Session name
683
+ include_patterns: Include filter
684
+ exclude_patterns: Exclude filter
685
+ respect_gitignore: Respect .gitignore
686
+ recursive: Recurse into subdirectories
687
+ Returns:
688
+ Count of files pinned.
689
+ """
690
+ root = Path(folder_path)
691
+ if not root.exists() or not root.is_dir():
692
+ return 0
693
+ from tenets.utils.scanner import FileScanner
694
+
695
+ scanner = FileScanner(self.config)
696
+ paths = [root]
697
+ files = scanner.scan(
698
+ paths,
699
+ include_patterns=include_patterns,
700
+ exclude_patterns=exclude_patterns,
701
+ follow_symlinks=False,
702
+ respect_gitignore=respect_gitignore,
703
+ )
704
+ count = 0
705
+ for f in files:
706
+ if self.add_file_to_session(f, session=session):
707
+ count += 1
708
+ return count
709
+
710
+ def list_tenets(
711
+ self,
712
+ pending_only: bool = False,
713
+ instilled_only: bool = False,
714
+ session: Optional[str] = None,
715
+ category: Optional[Union[str, TenetCategory]] = None,
716
+ ) -> list[dict[str, Any]]:
717
+ """List tenets with optional filtering.
718
+
719
+ Args:
720
+ pending_only: Only show pending (not yet instilled) tenets
721
+ instilled_only: Only show instilled tenets
722
+ session: Filter by session binding
723
+ category: Filter by category
724
+
725
+ Returns:
726
+ List of tenet dictionaries
727
+
728
+ Example:
729
+ >>> # List all tenets
730
+ >>> all_tenets = ten.list_tenets()
731
+ >>>
732
+ >>> # List only pending security tenets
733
+ >>> pending_security = ten.list_tenets(
734
+ ... pending_only=True,
735
+ ... category="security"
736
+ ... )
737
+ """
738
+ return self.tenet_manager.list_tenets(
739
+ pending_only=pending_only,
740
+ instilled_only=instilled_only,
741
+ session=session or self._session,
742
+ category=category,
743
+ )
744
+
745
+ def get_tenet(self, tenet_id: str) -> Optional[Tenet]:
746
+ """Get a specific tenet by ID.
747
+
748
+ Args:
749
+ tenet_id: Tenet ID (can be partial)
750
+
751
+ Returns:
752
+ The Tenet object or None if not found
753
+ """
754
+ return self.tenet_manager.get_tenet(tenet_id)
755
+
756
+ def remove_tenet(self, tenet_id: str) -> bool:
757
+ """Remove (archive) a tenet.
758
+
759
+ Args:
760
+ tenet_id: Tenet ID (can be partial)
761
+
762
+ Returns:
763
+ True if removed, False if not found
764
+ """
765
+ return self.tenet_manager.remove_tenet(tenet_id)
766
+
767
+ def get_pending_tenets(self, session: Optional[str] = None) -> List[Tenet]:
768
+ """Get all pending tenets.
769
+
770
+ Args:
771
+ session: Optional session filter
772
+
773
+ Returns:
774
+ List of pending Tenet objects
775
+ """
776
+ return self.tenet_manager.get_pending_tenets(session or self._session)
777
+
778
+ def export_tenets(self, format: str = "yaml", session: Optional[str] = None) -> str:
779
+ """Export tenets to YAML or JSON.
780
+
781
+ Args:
782
+ format: Export format - 'yaml' or 'json'
783
+ session: Optional session filter
784
+
785
+ Returns:
786
+ Serialized tenets string
787
+ """
788
+ return self.tenet_manager.export_tenets(format=format, session=session or self._session)
789
+
790
+ def import_tenets(self, file_path: Union[str, Path], session: Optional[str] = None) -> int:
791
+ """Import tenets from file.
792
+
793
+ Args:
794
+ file_path: Path to import file (YAML or JSON)
795
+ session: Optional session to bind imported tenets to
796
+
797
+ Returns:
798
+ Number of tenets imported
799
+ """
800
+ return self.tenet_manager.import_tenets(
801
+ file_path=file_path, session=session or self._session
802
+ )
803
+
804
+ # ============= Analysis Methods =============
805
+
806
+ def examine(
807
+ self,
808
+ path: Optional[Union[str, Path]] = None,
809
+ deep: bool = False,
810
+ include_git: bool = True,
811
+ output_metadata: bool = False,
812
+ ) -> Any: # Returns AnalysisResult
813
+ """Examine codebase structure and metrics.
814
+
815
+ Provides detailed analysis of your code including file counts, language
816
+ distribution, complexity metrics, and potential issues.
817
+
818
+ Args:
819
+ path: Path to examine (default: current directory)
820
+ deep: Perform deep analysis with AST parsing
821
+ include_git: Include git statistics
822
+ output_metadata: Include detailed metadata in result
823
+
824
+ Returns:
825
+ AnalysisResult object with comprehensive codebase analysis
826
+
827
+ Example:
828
+ >>> # Basic examination
829
+ >>> analysis = ten.examine()
830
+ >>> print(f"Found {analysis.total_files} files")
831
+ >>> print(f"Languages: {', '.join(analysis.languages)}")
832
+ >>>
833
+ >>> # Deep analysis with git
834
+ >>> analysis = ten.examine(deep=True, include_git=True)
835
+ """
836
+ # This would call the analyzer module (not shown in detail here)
837
+ # Placeholder for now
838
+ from tenets.core.analysis import CodeAnalyzer
839
+
840
+ analyzer = CodeAnalyzer(self.config)
841
+
842
+ # Would return proper AnalysisResult
843
+ return {
844
+ "total_files": 0,
845
+ "languages": [],
846
+ "message": "Examine functionality to be implemented",
847
+ }
848
+
849
+ def track_changes(
850
+ self,
851
+ path: Optional[Union[str, Path]] = None,
852
+ since: str = "1 week",
853
+ author: Optional[str] = None,
854
+ file_pattern: Optional[str] = None,
855
+ ) -> Dict[str, Any]:
856
+ """Track code changes over time.
857
+
858
+ Args:
859
+ path: Repository path (default: current directory)
860
+ since: Time period (e.g., '1 week', '3 days', 'yesterday')
861
+ author: Filter by author
862
+ file_pattern: Filter by file pattern
863
+
864
+ Returns:
865
+ Dictionary with change information
866
+ """
867
+ # Placeholder - would integrate with git module
868
+ return {
869
+ "commits": [],
870
+ "files": [],
871
+ "message": "Track changes functionality to be implemented",
872
+ }
873
+
874
+ def momentum(
875
+ self,
876
+ path: Optional[Union[str, Path]] = None,
877
+ since: str = "last-month",
878
+ team: bool = False,
879
+ author: Optional[str] = None,
880
+ ) -> Dict[str, Any]:
881
+ """Track development momentum and velocity.
882
+
883
+ Args:
884
+ path: Repository path
885
+ since: Time period to analyze
886
+ team: Show team-wide statistics
887
+ author: Show stats for specific author
888
+
889
+ Returns:
890
+ Dictionary with momentum metrics
891
+ """
892
+ # Placeholder - would integrate with git analyzer
893
+ return {"overall": {}, "weekly": [], "message": "Momentum functionality to be implemented"}
894
+
895
+ def estimate_cost(self, result: ContextResult, model: str) -> Dict[str, Any]:
896
+ """Estimate the cost of using generated context with an LLM.
897
+
898
+ Args:
899
+ result: ContextResult from distill()
900
+ model: Target model name
901
+
902
+ Returns:
903
+ Dictionary with token counts and cost estimates
904
+ """
905
+ from tenets.models.llm import estimate_cost as _estimate_cost
906
+ from tenets.models.llm import get_model_limits
907
+
908
+ input_tokens = result.token_count
909
+ # Use a conservative default for expected output if not specified elsewhere
910
+ default_output = get_model_limits(model).max_output
911
+ return _estimate_cost(input_tokens=input_tokens, output_tokens=default_output, model=model)
912
+
913
+ # ============= System Instruction Management =============
914
+
915
+ def set_system_instruction(
916
+ self,
917
+ instruction: str,
918
+ enable: bool = True,
919
+ position: str = "top",
920
+ format: str = "markdown",
921
+ save: bool = False,
922
+ ) -> None:
923
+ """Set the system instruction for AI interactions.
924
+
925
+ Args:
926
+ instruction: The system instruction text
927
+ enable: Whether to auto-inject
928
+ position: Where to inject ('top', 'after_header', 'before_content')
929
+ format: Format type ('markdown', 'xml', 'comment', 'plain')
930
+ save: Whether to save to config file
931
+ """
932
+ self.config.tenet.system_instruction = instruction
933
+ self.config.tenet.system_instruction_enabled = enable
934
+ self.config.tenet.system_instruction_position = position
935
+ self.config.tenet.system_instruction_format = format
936
+
937
+ if save and getattr(self.config, "config_file", None):
938
+ self.config.save()
939
+
940
+ self.logger.info(f"System instruction set ({len(instruction)} chars)")
941
+
942
+ def get_system_instruction(self) -> Optional[str]:
943
+ """Get the current system instruction.
944
+
945
+ Returns:
946
+ The system instruction text or None
947
+ """
948
+ return self.config.tenet.system_instruction
949
+
950
+ def clear_system_instruction(self, save: bool = False) -> None:
951
+ """Clear the system instruction.
952
+
953
+ Args:
954
+ save: Whether to save to config file
955
+ """
956
+ self.config.tenet.system_instruction = None
957
+ self.config.tenet.system_instruction_enabled = False
958
+
959
+ if save and getattr(self.config, "config_file", None):
960
+ self.config.save()
961
+
962
+ self.logger.info("System instruction cleared")
963
+
964
+
965
+ # Convenience exports
966
+ __all__ = [
967
+ "CodeAnalyzer",
968
+ "ContextResult",
969
+ "Distiller",
970
+ "Instiller",
971
+ "Priority",
972
+ "Tenet",
973
+ "TenetCategory",
974
+ "TenetManager",
975
+ "Tenets",
976
+ "TenetsConfig",
977
+ "__version__",
978
+ "get_logger",
979
+ ]