codebase-intel 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 (36) hide show
  1. codebase_intel/__init__.py +3 -0
  2. codebase_intel/analytics/__init__.py +1 -0
  3. codebase_intel/analytics/benchmark.py +406 -0
  4. codebase_intel/analytics/feedback.py +496 -0
  5. codebase_intel/analytics/tracker.py +439 -0
  6. codebase_intel/cli/__init__.py +1 -0
  7. codebase_intel/cli/main.py +740 -0
  8. codebase_intel/contracts/__init__.py +1 -0
  9. codebase_intel/contracts/auto_generator.py +438 -0
  10. codebase_intel/contracts/evaluator.py +531 -0
  11. codebase_intel/contracts/models.py +433 -0
  12. codebase_intel/contracts/registry.py +225 -0
  13. codebase_intel/core/__init__.py +1 -0
  14. codebase_intel/core/config.py +248 -0
  15. codebase_intel/core/exceptions.py +454 -0
  16. codebase_intel/core/types.py +375 -0
  17. codebase_intel/decisions/__init__.py +1 -0
  18. codebase_intel/decisions/miner.py +297 -0
  19. codebase_intel/decisions/models.py +302 -0
  20. codebase_intel/decisions/store.py +411 -0
  21. codebase_intel/drift/__init__.py +1 -0
  22. codebase_intel/drift/detector.py +443 -0
  23. codebase_intel/graph/__init__.py +1 -0
  24. codebase_intel/graph/builder.py +391 -0
  25. codebase_intel/graph/parser.py +1232 -0
  26. codebase_intel/graph/query.py +377 -0
  27. codebase_intel/graph/storage.py +736 -0
  28. codebase_intel/mcp/__init__.py +1 -0
  29. codebase_intel/mcp/server.py +710 -0
  30. codebase_intel/orchestrator/__init__.py +1 -0
  31. codebase_intel/orchestrator/assembler.py +649 -0
  32. codebase_intel-0.1.0.dist-info/METADATA +361 -0
  33. codebase_intel-0.1.0.dist-info/RECORD +36 -0
  34. codebase_intel-0.1.0.dist-info/WHEEL +4 -0
  35. codebase_intel-0.1.0.dist-info/entry_points.txt +2 -0
  36. codebase_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,248 @@
1
+ """Configuration system with validation and sensible defaults.
2
+
3
+ Edge cases handled:
4
+ - Config file doesn't exist: use defaults, create template on init
5
+ - Config file has unknown keys: warn but don't fail (forward compat)
6
+ - Config file has invalid values: raise ContractParseError with specifics
7
+ - Environment variables override file config (12-factor)
8
+ - Relative paths in config: resolved relative to project root
9
+ - Multiple config files: project-level overrides user-level overrides defaults
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from pydantic import Field, field_validator, model_validator
18
+ from pydantic_settings import BaseSettings, SettingsConfigDict
19
+
20
+ from codebase_intel.core.types import Language
21
+
22
+
23
+ class ParserConfig(BaseSettings):
24
+ """Tree-sitter parsing configuration."""
25
+
26
+ max_file_size_bytes: int = Field(
27
+ default=1_048_576, # 1MB
28
+ description="Skip files larger than this (generated code, bundles)",
29
+ )
30
+ timeout_ms: int = Field(
31
+ default=5_000,
32
+ description="Per-file parse timeout to handle pathological grammars",
33
+ )
34
+ enabled_languages: list[Language] = Field(
35
+ default=[
36
+ Language.PYTHON,
37
+ Language.JAVASCRIPT,
38
+ Language.TYPESCRIPT,
39
+ Language.TSX,
40
+ ],
41
+ )
42
+ ignored_patterns: list[str] = Field(
43
+ default=[
44
+ "node_modules/**",
45
+ ".git/**",
46
+ "__pycache__/**",
47
+ "*.min.js",
48
+ "*.bundle.js",
49
+ "*.generated.*",
50
+ "vendor/**",
51
+ "dist/**",
52
+ "build/**",
53
+ ".venv/**",
54
+ "venv/**",
55
+ ],
56
+ description="Glob patterns for files/dirs to skip entirely",
57
+ )
58
+ generated_markers: list[str] = Field(
59
+ default=[
60
+ "# generated",
61
+ "// generated",
62
+ "/* generated",
63
+ "@generated",
64
+ "DO NOT EDIT",
65
+ "auto-generated",
66
+ ],
67
+ description="If first 5 lines contain any marker, flag as generated",
68
+ )
69
+
70
+
71
+ class GraphConfig(BaseSettings):
72
+ """Code graph storage and behavior configuration."""
73
+
74
+ db_path: Path = Field(
75
+ default=Path(".codebase-intel/graph.db"),
76
+ description="SQLite database path (relative to project root)",
77
+ )
78
+ enable_wal_mode: bool = Field(
79
+ default=True,
80
+ description="WAL mode for concurrent read/write (git hook + MCP server)",
81
+ )
82
+ max_traversal_depth: int = Field(
83
+ default=10,
84
+ description="Max depth for dependency traversal to prevent runaway on cycles",
85
+ )
86
+ track_dynamic_imports: bool = Field(
87
+ default=True,
88
+ description="Attempt to resolve importlib / dynamic require (lower confidence)",
89
+ )
90
+ include_type_only_edges: bool = Field(
91
+ default=True,
92
+ description="Include TYPE_CHECKING / type-only imports in graph",
93
+ )
94
+
95
+
96
+ class DecisionConfig(BaseSettings):
97
+ """Decision journal configuration."""
98
+
99
+ decisions_dir: Path = Field(
100
+ default=Path(".codebase-intel/decisions"),
101
+ description="Directory for decision YAML files",
102
+ )
103
+ auto_mine_git: bool = Field(
104
+ default=True,
105
+ description="Auto-suggest decisions from PR descriptions and commit messages",
106
+ )
107
+ staleness_threshold_days: int = Field(
108
+ default=90,
109
+ description="Flag decisions not reviewed in this many days",
110
+ )
111
+ max_linked_code_regions: int = Field(
112
+ default=20,
113
+ description="Max code anchors per decision (prevent over-linking)",
114
+ )
115
+ mine_pr_labels: list[str] = Field(
116
+ default=["architecture", "adr", "decision", "breaking-change"],
117
+ description="PR labels that suggest a decision worth recording",
118
+ )
119
+
120
+
121
+ class ContractConfig(BaseSettings):
122
+ """Quality contract configuration."""
123
+
124
+ contracts_dir: Path = Field(
125
+ default=Path(".codebase-intel/contracts"),
126
+ description="Directory for contract YAML files",
127
+ )
128
+ enable_builtin_contracts: bool = Field(
129
+ default=True,
130
+ description="Include default contracts (no N+1, layer violations, etc.)",
131
+ )
132
+ strict_mode: bool = Field(
133
+ default=False,
134
+ description="Treat WARNING-level violations as ERROR",
135
+ )
136
+
137
+
138
+ class OrchestratorConfig(BaseSettings):
139
+ """Context orchestrator configuration."""
140
+
141
+ default_budget_tokens: int = Field(
142
+ default=8_000,
143
+ description="Default token budget if agent doesn't specify",
144
+ )
145
+ min_useful_tokens: int = Field(
146
+ default=500,
147
+ description="Below this budget, return metadata only (no file content)",
148
+ )
149
+ max_assembly_time_ms: int = Field(
150
+ default=5_000,
151
+ description="Timeout for context assembly to keep MCP responses fast",
152
+ )
153
+ include_stale_context: bool = Field(
154
+ default=True,
155
+ description="Include stale items with low freshness score (vs. dropping them)",
156
+ )
157
+ freshness_decay_days: int = Field(
158
+ default=30,
159
+ description="Context freshness drops to 0.5 after this many days without validation",
160
+ )
161
+
162
+
163
+ class DriftConfig(BaseSettings):
164
+ """Drift detection configuration."""
165
+
166
+ rot_threshold_pct: float = Field(
167
+ default=0.3,
168
+ description="Flag context rot event if this % of records are stale",
169
+ )
170
+ check_on_commit: bool = Field(
171
+ default=True,
172
+ description="Run drift check as post-commit hook",
173
+ )
174
+ ignore_generated_files: bool = Field(
175
+ default=True,
176
+ description="Don't flag drift for generated code changes",
177
+ )
178
+
179
+
180
+ class ProjectConfig(BaseSettings):
181
+ """Top-level project configuration aggregating all module configs.
182
+
183
+ Resolution order:
184
+ 1. Environment variables (CODEBASE_INTEL_*)
185
+ 2. Project config file (.codebase-intel/config.yaml)
186
+ 3. User config file (~/.config/codebase-intel/config.yaml)
187
+ 4. Defaults defined here
188
+
189
+ Edge cases:
190
+ - Config file with YAML syntax errors: raise with file path + line number
191
+ - Config file with wrong types: Pydantic validation catches with clear errors
192
+ - Missing config file: use defaults (tool works out of the box)
193
+ - Config file in git: yes, it should be committed (project-specific settings)
194
+ """
195
+
196
+ model_config = SettingsConfigDict(
197
+ env_prefix="CODEBASE_INTEL_",
198
+ env_nested_delimiter="__",
199
+ )
200
+
201
+ project_root: Path = Field(default=Path("."))
202
+ parser: ParserConfig = Field(default_factory=ParserConfig)
203
+ graph: GraphConfig = Field(default_factory=GraphConfig)
204
+ decisions: DecisionConfig = Field(default_factory=DecisionConfig)
205
+ contracts: ContractConfig = Field(default_factory=ContractConfig)
206
+ orchestrator: OrchestratorConfig = Field(default_factory=OrchestratorConfig)
207
+ drift: DriftConfig = Field(default_factory=DriftConfig)
208
+
209
+ @field_validator("project_root")
210
+ @classmethod
211
+ def resolve_project_root(cls, v: Path) -> Path:
212
+ resolved = v.resolve()
213
+ if not resolved.is_dir():
214
+ msg = f"Project root does not exist: {resolved}"
215
+ raise ValueError(msg)
216
+ return resolved
217
+
218
+ @model_validator(mode="after")
219
+ def resolve_relative_paths(self) -> ProjectConfig:
220
+ """Resolve all relative paths against project_root.
221
+
222
+ Edge case: user specifies graph.db_path as "../shared/graph.db"
223
+ for a monorepo setup. We resolve it but verify the parent dir exists.
224
+ """
225
+ root = self.project_root
226
+
227
+ def _resolve(p: Path) -> Path:
228
+ return p if p.is_absolute() else (root / p).resolve()
229
+
230
+ # Mutate nested configs (Pydantic v2 allows this in validators)
231
+ object.__setattr__(self.graph, "db_path", _resolve(self.graph.db_path))
232
+ object.__setattr__(
233
+ self.decisions, "decisions_dir", _resolve(self.decisions.decisions_dir)
234
+ )
235
+ object.__setattr__(
236
+ self.contracts, "contracts_dir", _resolve(self.contracts.contracts_dir)
237
+ )
238
+ return self
239
+
240
+ def ensure_dirs(self) -> None:
241
+ """Create required directories if they don't exist."""
242
+ self.graph.db_path.parent.mkdir(parents=True, exist_ok=True)
243
+ self.decisions.decisions_dir.mkdir(parents=True, exist_ok=True)
244
+ self.contracts.contracts_dir.mkdir(parents=True, exist_ok=True)
245
+
246
+ def to_yaml_dict(self) -> dict[str, Any]:
247
+ """Export as a dict suitable for YAML serialization (config template)."""
248
+ return self.model_dump(mode="json", exclude={"project_root"})
@@ -0,0 +1,454 @@
1
+ """Exception hierarchy for codebase-intel.
2
+
3
+ Design principles:
4
+ - Every exception carries structured context (not just a message string)
5
+ - Exceptions map to specific failure modes, never generic catch-alls
6
+ - Each exception knows whether it's retryable and what recovery action to suggest
7
+ - Exceptions are grouped by module to avoid collision and aid filtering
8
+
9
+ Edge cases handled:
10
+ - Partial initialization (graph exists but decisions don't)
11
+ - Concurrent access conflicts (two processes updating the same graph)
12
+ - Corrupt storage (SQLite file damaged, YAML malformed)
13
+ - Resource exhaustion (token budget exceeded, file too large)
14
+ - External dependency failures (tree-sitter crash, git unavailable)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass, field
20
+ from enum import Enum
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+
25
+ class Severity(Enum):
26
+ """How urgently this error needs attention."""
27
+
28
+ WARNING = "warning" # Degraded but functional — log and continue
29
+ ERROR = "error" # Operation failed — caller must handle
30
+ FATAL = "fatal" # System unusable — abort and report
31
+
32
+
33
+ class RecoveryHint(Enum):
34
+ """Machine-readable recovery suggestions for agents and CLI."""
35
+
36
+ RETRY = "retry" # Transient failure, try again
37
+ REINITIALIZE = "reinitialize" # Run `codebase-intel init` again
38
+ REDUCE_SCOPE = "reduce_scope" # Narrow the query or task
39
+ MANUAL_FIX = "manual_fix" # Human intervention needed
40
+ SKIP = "skip" # Safe to skip this item and continue
41
+ UPDATE_CONFIG = "update_config" # Configuration needs adjustment
42
+
43
+
44
+ @dataclass
45
+ class ErrorContext:
46
+ """Structured context attached to every exception.
47
+
48
+ Agents can parse this to make intelligent recovery decisions
49
+ rather than pattern-matching on error message strings.
50
+ """
51
+
52
+ file_path: Path | None = None
53
+ line_range: tuple[int, int] | None = None
54
+ component: str = "" # Which module raised this
55
+ operation: str = "" # What was being attempted
56
+ details: dict[str, Any] = field(default_factory=dict)
57
+
58
+
59
+ class CodebaseIntelError(Exception):
60
+ """Base exception — all module exceptions inherit from this.
61
+
62
+ Every exception in the system carries:
63
+ - severity: how bad is it
64
+ - recovery: what should the caller do
65
+ - context: structured data about what went wrong
66
+ """
67
+
68
+ severity: Severity = Severity.ERROR
69
+ recovery: RecoveryHint = RecoveryHint.MANUAL_FIX
70
+
71
+ def __init__(self, message: str, context: ErrorContext | None = None) -> None:
72
+ super().__init__(message)
73
+ self.context = context or ErrorContext()
74
+
75
+ def to_dict(self) -> dict[str, Any]:
76
+ """Serialize for MCP/JSON responses."""
77
+ return {
78
+ "error": type(self).__name__,
79
+ "message": str(self),
80
+ "severity": self.severity.value,
81
+ "recovery": self.recovery.value,
82
+ "context": {
83
+ "file_path": str(self.context.file_path) if self.context.file_path else None,
84
+ "line_range": self.context.line_range,
85
+ "component": self.context.component,
86
+ "operation": self.context.operation,
87
+ "details": self.context.details,
88
+ },
89
+ }
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Storage errors
94
+ # ---------------------------------------------------------------------------
95
+
96
+
97
+ class StorageError(CodebaseIntelError):
98
+ """Base for all storage-layer failures."""
99
+
100
+ def __init__(self, message: str, context: ErrorContext | None = None) -> None:
101
+ ctx = context or ErrorContext()
102
+ ctx.component = ctx.component or "storage"
103
+ super().__init__(message, ctx)
104
+
105
+
106
+ class StorageCorruptError(StorageError):
107
+ """SQLite file or YAML document is unreadable.
108
+
109
+ Edge case: partially written file from a crash mid-write.
110
+ Recovery: re-initialize from scratch (data can be rebuilt from git).
111
+ """
112
+
113
+ severity = Severity.ERROR
114
+ recovery = RecoveryHint.REINITIALIZE
115
+
116
+
117
+ class StorageConcurrencyError(StorageError):
118
+ """Two processes tried to write simultaneously.
119
+
120
+ Edge case: git hook and MCP server both updating the graph.
121
+ SQLite handles this via WAL mode, but we still need to detect
122
+ and retry at the application level.
123
+ """
124
+
125
+ severity = Severity.WARNING
126
+ recovery = RecoveryHint.RETRY
127
+
128
+
129
+ class StorageMigrationError(StorageError):
130
+ """Database schema version mismatch.
131
+
132
+ Edge case: user updated codebase-intel but hasn't migrated
133
+ their existing database. We must not silently drop data.
134
+ """
135
+
136
+ severity = Severity.ERROR
137
+ recovery = RecoveryHint.REINITIALIZE
138
+
139
+
140
+ # ---------------------------------------------------------------------------
141
+ # Graph errors
142
+ # ---------------------------------------------------------------------------
143
+
144
+
145
+ class GraphError(CodebaseIntelError):
146
+ """Base for code graph failures."""
147
+
148
+ def __init__(self, message: str, context: ErrorContext | None = None) -> None:
149
+ ctx = context or ErrorContext()
150
+ ctx.component = ctx.component or "graph"
151
+ super().__init__(message, ctx)
152
+
153
+
154
+ class ParseError(GraphError):
155
+ """Tree-sitter failed to parse a file.
156
+
157
+ Edge cases:
158
+ - Binary file misidentified as source code
159
+ - File with mixed encodings (UTF-8 + Latin-1 in same file)
160
+ - Syntax errors in user's code (must still extract partial info)
161
+ - Generated code with unusual constructs (protobuf, codegen)
162
+ - Files exceeding the size threshold (e.g., 10MB generated file)
163
+
164
+ Recovery: skip this file, log warning, continue with partial graph.
165
+ The graph must be useful even if some files can't be parsed.
166
+ """
167
+
168
+ severity = Severity.WARNING
169
+ recovery = RecoveryHint.SKIP
170
+
171
+
172
+ class CircularDependencyError(GraphError):
173
+ """Detected a circular dependency chain.
174
+
175
+ Edge case: A → B → C → A. This is legitimate in many codebases
176
+ (Python allows it with deferred imports). We must:
177
+ 1. Record the cycle in the graph (it's real information)
178
+ 2. Not infinite-loop during traversal
179
+ 3. Flag it as a quality concern without blocking analysis
180
+
181
+ This is a WARNING, not an ERROR — cycles are common and valid.
182
+ """
183
+
184
+ severity = Severity.WARNING
185
+ recovery = RecoveryHint.SKIP
186
+
187
+ def __init__(
188
+ self, cycle_path: list[str], context: ErrorContext | None = None
189
+ ) -> None:
190
+ self.cycle_path = cycle_path
191
+ message = f"Circular dependency: {' → '.join(cycle_path)}"
192
+ super().__init__(message, context)
193
+
194
+
195
+ class UnsupportedLanguageError(GraphError):
196
+ """No tree-sitter grammar available for this file type.
197
+
198
+ Edge case: user has .proto, .graphql, .sql files — we can't parse them
199
+ but we should still track them as nodes in the graph (without internal
200
+ structure). The graph should degrade gracefully, not fail entirely.
201
+ """
202
+
203
+ severity = Severity.WARNING
204
+ recovery = RecoveryHint.SKIP
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Decision errors
209
+ # ---------------------------------------------------------------------------
210
+
211
+
212
+ class DecisionError(CodebaseIntelError):
213
+ """Base for decision journal failures."""
214
+
215
+ def __init__(self, message: str, context: ErrorContext | None = None) -> None:
216
+ ctx = context or ErrorContext()
217
+ ctx.component = ctx.component or "decisions"
218
+ super().__init__(message, ctx)
219
+
220
+
221
+ class DecisionConflictError(DecisionError):
222
+ """Two active decisions contradict each other.
223
+
224
+ Edge case: DEC-042 says "use token bucket" and DEC-058 says
225
+ "use sliding window" for the same module. This happens when:
226
+ - Different team members made decisions at different times
227
+ - A decision was superseded but not marked as such
228
+ - Cross-repo decisions conflict
229
+
230
+ Recovery: surface both to the agent with conflict metadata
231
+ so it can ask the developer which one to follow.
232
+ """
233
+
234
+ severity = Severity.WARNING
235
+ recovery = RecoveryHint.MANUAL_FIX
236
+
237
+ def __init__(
238
+ self,
239
+ decision_a: str,
240
+ decision_b: str,
241
+ context: ErrorContext | None = None,
242
+ ) -> None:
243
+ self.decision_a = decision_a
244
+ self.decision_b = decision_b
245
+ message = f"Conflicting decisions: {decision_a} vs {decision_b}"
246
+ super().__init__(message, context)
247
+
248
+
249
+ class StaleDecisionError(DecisionError):
250
+ """Decision references code that has changed significantly.
251
+
252
+ Edge case: decision links to src/auth/middleware.py:15-82
253
+ but that file was refactored and those lines now contain
254
+ completely different code. The decision may still be valid
255
+ (the logic moved) or may be obsolete.
256
+
257
+ We measure staleness by content hash comparison, not just
258
+ line number drift.
259
+ """
260
+
261
+ severity = Severity.WARNING
262
+ recovery = RecoveryHint.MANUAL_FIX
263
+
264
+
265
+ class OrphanedDecisionError(DecisionError):
266
+ """Decision links to files/functions that no longer exist.
267
+
268
+ Edge case: file was deleted or function was removed during refactor.
269
+ The decision might still carry useful architectural context even
270
+ though its code anchor is gone.
271
+ """
272
+
273
+ severity = Severity.WARNING
274
+ recovery = RecoveryHint.MANUAL_FIX
275
+
276
+
277
+ # ---------------------------------------------------------------------------
278
+ # Contract errors
279
+ # ---------------------------------------------------------------------------
280
+
281
+
282
+ class ContractError(CodebaseIntelError):
283
+ """Base for quality contract failures."""
284
+
285
+ def __init__(self, message: str, context: ErrorContext | None = None) -> None:
286
+ ctx = context or ErrorContext()
287
+ ctx.component = ctx.component or "contracts"
288
+ super().__init__(message, ctx)
289
+
290
+
291
+ class ContractViolationError(ContractError):
292
+ """Code violates a quality contract.
293
+
294
+ This is the most common "error" — it's really a signal, not a failure.
295
+ The agent should see this as guidance, not a crash.
296
+ """
297
+
298
+ severity = Severity.WARNING
299
+ recovery = RecoveryHint.MANUAL_FIX
300
+
301
+ def __init__(
302
+ self,
303
+ contract_id: str,
304
+ rule: str,
305
+ violation_detail: str,
306
+ context: ErrorContext | None = None,
307
+ ) -> None:
308
+ self.contract_id = contract_id
309
+ self.rule = rule
310
+ self.violation_detail = violation_detail
311
+ message = f"[{contract_id}] {rule}: {violation_detail}"
312
+ super().__init__(message, context)
313
+
314
+
315
+ class ContractConflictError(ContractError):
316
+ """Two contracts impose contradictory requirements.
317
+
318
+ Edge case: Contract A says "max function length 50 lines" and
319
+ Contract B says "no helper functions for single-use logic."
320
+ For complex logic, these conflict.
321
+
322
+ Also: architectural rule says "no direct DB access outside repositories"
323
+ but a performance contract says "use raw SQL for bulk operations."
324
+
325
+ Resolution: contracts have priority levels. Higher-priority wins.
326
+ If same priority, flag as conflict.
327
+ """
328
+
329
+ severity = Severity.WARNING
330
+ recovery = RecoveryHint.MANUAL_FIX
331
+
332
+
333
+ class ContractParseError(ContractError):
334
+ """Contract definition file has invalid syntax.
335
+
336
+ Edge case: user hand-edited a YAML contract file and introduced
337
+ a syntax error. Must not crash — report which contract is broken
338
+ and continue evaluating the rest.
339
+ """
340
+
341
+ severity = Severity.ERROR
342
+ recovery = RecoveryHint.UPDATE_CONFIG
343
+
344
+
345
+ # ---------------------------------------------------------------------------
346
+ # Orchestrator errors
347
+ # ---------------------------------------------------------------------------
348
+
349
+
350
+ class OrchestratorError(CodebaseIntelError):
351
+ """Base for context assembly failures."""
352
+
353
+ def __init__(self, message: str, context: ErrorContext | None = None) -> None:
354
+ ctx = context or ErrorContext()
355
+ ctx.component = ctx.component or "orchestrator"
356
+ super().__init__(message, ctx)
357
+
358
+
359
+ class BudgetExceededError(OrchestratorError):
360
+ """Relevant context exceeds the agent's token budget.
361
+
362
+ Edge case: task touches a core module that 200 files depend on.
363
+ Even the minimum relevant context is 50K tokens but the agent
364
+ only has 8K budget.
365
+
366
+ Recovery: orchestrator must prioritize ruthlessly — closest
367
+ dependencies first, most recent decisions first, highest-priority
368
+ contracts first. Return what fits + a "truncated" flag.
369
+ """
370
+
371
+ severity = Severity.WARNING
372
+ recovery = RecoveryHint.REDUCE_SCOPE
373
+
374
+ def __init__(
375
+ self,
376
+ budget_tokens: int,
377
+ required_tokens: int,
378
+ context: ErrorContext | None = None,
379
+ ) -> None:
380
+ self.budget_tokens = budget_tokens
381
+ self.required_tokens = required_tokens
382
+ message = (
383
+ f"Context requires ~{required_tokens} tokens "
384
+ f"but budget is {budget_tokens}"
385
+ )
386
+ super().__init__(message, context)
387
+
388
+
389
+ class PartialInitializationError(OrchestratorError):
390
+ """Some components are initialized but not others.
391
+
392
+ Edge case: user ran `init` but it crashed halfway — graph exists
393
+ but decisions database doesn't. The orchestrator must work with
394
+ whatever is available and clearly indicate what's missing.
395
+ """
396
+
397
+ severity = Severity.WARNING
398
+ recovery = RecoveryHint.REINITIALIZE
399
+
400
+ def __init__(
401
+ self,
402
+ available: list[str],
403
+ missing: list[str],
404
+ context: ErrorContext | None = None,
405
+ ) -> None:
406
+ self.available = available
407
+ self.missing = missing
408
+ message = (
409
+ f"Partial init — available: {available}, missing: {missing}"
410
+ )
411
+ super().__init__(message, context)
412
+
413
+
414
+ # ---------------------------------------------------------------------------
415
+ # Drift errors
416
+ # ---------------------------------------------------------------------------
417
+
418
+
419
+ class DriftError(CodebaseIntelError):
420
+ """Base for drift detection failures."""
421
+
422
+ def __init__(self, message: str, context: ErrorContext | None = None) -> None:
423
+ ctx = context or ErrorContext()
424
+ ctx.component = ctx.component or "drift"
425
+ super().__init__(message, ctx)
426
+
427
+
428
+ class ContextRotError(DriftError):
429
+ """Context records have drifted significantly from actual code.
430
+
431
+ Edge case: after a major refactor, 40% of decision records
432
+ reference moved/renamed files. The system detects this level
433
+ of drift and flags it as a "context rot event" requiring
434
+ bulk review, rather than 200 individual warnings.
435
+ """
436
+
437
+ severity = Severity.ERROR
438
+ recovery = RecoveryHint.REINITIALIZE
439
+
440
+ def __init__(
441
+ self,
442
+ rot_percentage: float,
443
+ stale_records: int,
444
+ total_records: int,
445
+ context: ErrorContext | None = None,
446
+ ) -> None:
447
+ self.rot_percentage = rot_percentage
448
+ self.stale_records = stale_records
449
+ self.total_records = total_records
450
+ message = (
451
+ f"Context rot: {rot_percentage:.0%} of records stale "
452
+ f"({stale_records}/{total_records})"
453
+ )
454
+ super().__init__(message, context)