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.
- tenets/__init__.py +979 -0
- tenets/__main__.py +13 -0
- tenets/cli/__init__.py +27 -0
- tenets/cli/__main__.py +13 -0
- tenets/cli/app.py +243 -0
- tenets/cli/commands/__init__.py +14 -0
- tenets/cli/commands/_utils.py +16 -0
- tenets/cli/commands/chronicle.py +495 -0
- tenets/cli/commands/config.py +667 -0
- tenets/cli/commands/distill.py +596 -0
- tenets/cli/commands/examine.py +642 -0
- tenets/cli/commands/instill.py +776 -0
- tenets/cli/commands/momentum.py +763 -0
- tenets/cli/commands/rank.py +1337 -0
- tenets/cli/commands/session.py +297 -0
- tenets/cli/commands/system_instruction.py +476 -0
- tenets/cli/commands/tenet.py +784 -0
- tenets/cli/commands/viz.py +1144 -0
- tenets/config.py +1685 -0
- tenets/core/__init__.py +47 -0
- tenets/core/analysis/__init__.py +11 -0
- tenets/core/analysis/analyzer.py +1307 -0
- tenets/core/analysis/base.py +201 -0
- tenets/core/analysis/implementations/__init__.py +65 -0
- tenets/core/analysis/implementations/cpp_analyzer.py +1097 -0
- tenets/core/analysis/implementations/csharp_analyzer.py +1533 -0
- tenets/core/analysis/implementations/css_analyzer.py +1305 -0
- tenets/core/analysis/implementations/dart_analyzer.py +1348 -0
- tenets/core/analysis/implementations/gdscript_analyzer.py +946 -0
- tenets/core/analysis/implementations/generic_analyzer.py +1791 -0
- tenets/core/analysis/implementations/go_analyzer.py +1022 -0
- tenets/core/analysis/implementations/html_analyzer.py +910 -0
- tenets/core/analysis/implementations/java_analyzer.py +1017 -0
- tenets/core/analysis/implementations/javascript_analyzer.py +1021 -0
- tenets/core/analysis/implementations/kotlin_analyzer.py +1126 -0
- tenets/core/analysis/implementations/php_analyzer.py +1259 -0
- tenets/core/analysis/implementations/python_analyzer.py +1006 -0
- tenets/core/analysis/implementations/ruby_analyzer.py +1138 -0
- tenets/core/analysis/implementations/rust_analyzer.py +1185 -0
- tenets/core/analysis/implementations/scala_analyzer.py +1211 -0
- tenets/core/analysis/implementations/swift_analyzer.py +1247 -0
- tenets/core/analysis/project_detector.py +339 -0
- tenets/core/distiller/__init__.py +21 -0
- tenets/core/distiller/aggregator.py +410 -0
- tenets/core/distiller/distiller.py +468 -0
- tenets/core/distiller/formatter.py +1485 -0
- tenets/core/distiller/optimizer.py +322 -0
- tenets/core/distiller/transform.py +205 -0
- tenets/core/examiner/__init__.py +282 -0
- tenets/core/examiner/complexity.py +1148 -0
- tenets/core/examiner/examiner.py +767 -0
- tenets/core/examiner/hotspots.py +1914 -0
- tenets/core/examiner/metrics.py +758 -0
- tenets/core/examiner/ownership.py +1003 -0
- tenets/core/git/__init__.py +501 -0
- tenets/core/git/analyzer.py +517 -0
- tenets/core/git/blame.py +977 -0
- tenets/core/git/chronicle.py +1111 -0
- tenets/core/git/stats.py +1132 -0
- tenets/core/instiller/__init__.py +18 -0
- tenets/core/instiller/injector.py +507 -0
- tenets/core/instiller/instiller.py +1419 -0
- tenets/core/instiller/manager.py +649 -0
- tenets/core/momentum/__init__.py +448 -0
- tenets/core/momentum/metrics.py +833 -0
- tenets/core/momentum/tracker.py +1569 -0
- tenets/core/nlp/__init__.py +165 -0
- tenets/core/nlp/bm25.py +572 -0
- tenets/core/nlp/cache.py +194 -0
- tenets/core/nlp/embeddings.py +284 -0
- tenets/core/nlp/keyword_extractor.py +1107 -0
- tenets/core/nlp/ml_utils.py +365 -0
- tenets/core/nlp/programming_patterns.py +493 -0
- tenets/core/nlp/similarity.py +318 -0
- tenets/core/nlp/stopwords.py +235 -0
- tenets/core/nlp/tfidf.py +170 -0
- tenets/core/nlp/tokenizer.py +201 -0
- tenets/core/prompt/__init__.py +354 -0
- tenets/core/prompt/cache.py +494 -0
- tenets/core/prompt/entity_recognizer.py +950 -0
- tenets/core/prompt/external_sources.py +30 -0
- tenets/core/prompt/intent_detector.py +941 -0
- tenets/core/prompt/normalizer.py +111 -0
- tenets/core/prompt/parser.py +1584 -0
- tenets/core/prompt/temporal_parser.py +1014 -0
- tenets/core/ranking/__init__.py +304 -0
- tenets/core/ranking/factors.py +525 -0
- tenets/core/ranking/ranker.py +881 -0
- tenets/core/ranking/strategies.py +958 -0
- tenets/core/reporting/__init__.py +653 -0
- tenets/core/reporting/generator.py +1506 -0
- tenets/core/reporting/html_reporter.py +1419 -0
- tenets/core/reporting/markdown_reporter.py +726 -0
- tenets/core/reporting/visualizer.py +1056 -0
- tenets/core/session/__init__.py +1 -0
- tenets/core/session/session.py +99 -0
- tenets/core/summarizer/__init__.py +409 -0
- tenets/core/summarizer/llm.py +472 -0
- tenets/core/summarizer/strategies.py +837 -0
- tenets/core/summarizer/summarizer.py +1691 -0
- tenets/data/pattterns/entity_patterns.json +1317 -0
- tenets/data/pattterns/external_patterns.json +673 -0
- tenets/data/pattterns/intent_patterns.json +378 -0
- tenets/data/pattterns/programming_patterns.json +417 -0
- tenets/data/pattterns/temporal_patterns.json +751 -0
- tenets/data/stopwords/minimal.txt +48 -0
- tenets/data/stopwords/prompt_aggressive.txt +369 -0
- tenets/models/__init__.py +85 -0
- tenets/models/analysis.py +1100 -0
- tenets/models/context.py +490 -0
- tenets/models/llm.py +143 -0
- tenets/models/summary.py +436 -0
- tenets/models/tenet.py +340 -0
- tenets/storage/__init__.py +19 -0
- tenets/storage/cache.py +436 -0
- tenets/storage/session_db.py +284 -0
- tenets/storage/sqlite.py +131 -0
- tenets/utils/__init__.py +16 -0
- tenets/utils/external_sources.py +895 -0
- tenets/utils/logger.py +177 -0
- tenets/utils/multiprocessing.py +147 -0
- tenets/utils/scanner.py +431 -0
- tenets/utils/timing.py +650 -0
- tenets/utils/tokens.py +296 -0
- tenets/viz/__init__.py +686 -0
- tenets/viz/base.py +737 -0
- tenets/viz/complexity.py +442 -0
- tenets/viz/contributors.py +438 -0
- tenets/viz/coupling.py +459 -0
- tenets/viz/dependencies.py +528 -0
- tenets/viz/displays.py +423 -0
- tenets/viz/graph_generator.py +929 -0
- tenets/viz/hotspots.py +414 -0
- tenets/viz/momentum.py +439 -0
- tenets-0.1.0.dist-info/METADATA +414 -0
- tenets-0.1.0.dist-info/RECORD +139 -0
- tenets-0.1.0.dist-info/WHEEL +4 -0
- tenets-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|