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.
- codebase_intel/__init__.py +3 -0
- codebase_intel/analytics/__init__.py +1 -0
- codebase_intel/analytics/benchmark.py +406 -0
- codebase_intel/analytics/feedback.py +496 -0
- codebase_intel/analytics/tracker.py +439 -0
- codebase_intel/cli/__init__.py +1 -0
- codebase_intel/cli/main.py +740 -0
- codebase_intel/contracts/__init__.py +1 -0
- codebase_intel/contracts/auto_generator.py +438 -0
- codebase_intel/contracts/evaluator.py +531 -0
- codebase_intel/contracts/models.py +433 -0
- codebase_intel/contracts/registry.py +225 -0
- codebase_intel/core/__init__.py +1 -0
- codebase_intel/core/config.py +248 -0
- codebase_intel/core/exceptions.py +454 -0
- codebase_intel/core/types.py +375 -0
- codebase_intel/decisions/__init__.py +1 -0
- codebase_intel/decisions/miner.py +297 -0
- codebase_intel/decisions/models.py +302 -0
- codebase_intel/decisions/store.py +411 -0
- codebase_intel/drift/__init__.py +1 -0
- codebase_intel/drift/detector.py +443 -0
- codebase_intel/graph/__init__.py +1 -0
- codebase_intel/graph/builder.py +391 -0
- codebase_intel/graph/parser.py +1232 -0
- codebase_intel/graph/query.py +377 -0
- codebase_intel/graph/storage.py +736 -0
- codebase_intel/mcp/__init__.py +1 -0
- codebase_intel/mcp/server.py +710 -0
- codebase_intel/orchestrator/__init__.py +1 -0
- codebase_intel/orchestrator/assembler.py +649 -0
- codebase_intel-0.1.0.dist-info/METADATA +361 -0
- codebase_intel-0.1.0.dist-info/RECORD +36 -0
- codebase_intel-0.1.0.dist-info/WHEEL +4 -0
- codebase_intel-0.1.0.dist-info/entry_points.txt +2 -0
- 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)
|