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,433 @@
1
+ """Quality contract models — the schema for expressing "what good looks like."
2
+
3
+ A quality contract defines rules that code should follow. Unlike linters
4
+ (which check syntax/style), contracts enforce architectural patterns,
5
+ project-specific conventions, and anti-pattern avoidance.
6
+
7
+ Three layers:
8
+ 1. Architectural constraints: structural rules (layer violations, dependency direction)
9
+ 2. Pattern library: approved and forbidden code patterns
10
+ 3. Quality gates: measurable thresholds (complexity, coverage, function length)
11
+
12
+ Edge cases in design:
13
+ - Conflicting contracts: Contract A says "max 50 lines per function" but Contract B
14
+ says "no helper functions for one-time logic." For a 60-line function, these conflict.
15
+ Resolution: priority system (P0 > P1 > P2). Same priority = flag as conflict.
16
+ - Scope exclusions: "all code except tests", "only src/api/**"
17
+ - Gradual migration: old pattern and new pattern coexist during transition.
18
+ Contracts support a `migration_deadline` after which the old pattern is an error.
19
+ - Framework-specific: a "React patterns" contract pack shouldn't fire on Python files.
20
+ - Runtime-dependent rules: "no N+1 queries" — can only partially check statically
21
+ (detect loop+query pattern, but may false-positive on batched queries).
22
+ - AI-specific anti-patterns: rules targeting common AI generation mistakes.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from datetime import UTC, datetime
28
+ from enum import Enum
29
+ from pathlib import Path
30
+ from typing import Any
31
+
32
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
33
+
34
+ from codebase_intel.core.types import ContractSeverity, Language
35
+
36
+
37
+ class RuleKind(str, Enum):
38
+ """Types of quality rules."""
39
+
40
+ ARCHITECTURAL = "architectural" # Structural: layer violations, dependency direction
41
+ PATTERN = "pattern" # Code pattern: approved vs forbidden constructs
42
+ THRESHOLD = "threshold" # Measurable: complexity, length, coverage
43
+ AI_ANTIPATTERN = "ai_antipattern" # AI-specific: hallucinated imports, over-abstraction
44
+
45
+
46
+ class ScopeFilter(BaseModel):
47
+ """Defines which files/modules a contract applies to.
48
+
49
+ Edge cases:
50
+ - Empty include list: contract applies to everything (minus excludes)
51
+ - Only exclude list: applies to everything except the excluded paths
52
+ - Both include and exclude: include first, then subtract excludes
53
+ - Language filter: only applies to files of specific languages
54
+ - Directory depth: "src/api/**" vs "src/api/*" (recursive vs single level)
55
+ """
56
+
57
+ model_config = ConfigDict(frozen=True)
58
+
59
+ include_patterns: list[str] = Field(
60
+ default_factory=list,
61
+ description="Glob patterns for files this contract applies to. Empty = all files.",
62
+ )
63
+ exclude_patterns: list[str] = Field(
64
+ default_factory=lambda: [
65
+ "node_modules/**",
66
+ "*.min.js",
67
+ "*.generated.*",
68
+ "vendor/**",
69
+ "dist/**",
70
+ ],
71
+ description="Glob patterns for files to exclude from this contract.",
72
+ )
73
+ languages: list[Language] = Field(
74
+ default_factory=list,
75
+ description="Restrict to specific languages. Empty = all languages.",
76
+ )
77
+ exclude_tests: bool = Field(
78
+ default=False,
79
+ description="Exclude test files from this contract.",
80
+ )
81
+ exclude_generated: bool = Field(
82
+ default=True,
83
+ description="Exclude generated files from this contract.",
84
+ )
85
+
86
+ def matches(
87
+ self,
88
+ file_path: Path,
89
+ language: Language = Language.UNKNOWN,
90
+ is_test: bool = False,
91
+ is_generated: bool = False,
92
+ ) -> bool:
93
+ """Check if a file falls within this contract's scope."""
94
+ import fnmatch
95
+
96
+ if self.exclude_tests and is_test:
97
+ return False
98
+ if self.exclude_generated and is_generated:
99
+ return False
100
+ if self.languages and language not in self.languages:
101
+ return False
102
+
103
+ path_str = str(file_path)
104
+
105
+ # Check excludes first
106
+ if any(fnmatch.fnmatch(path_str, p) for p in self.exclude_patterns):
107
+ return False
108
+
109
+ # If no include patterns, match everything not excluded
110
+ if not self.include_patterns:
111
+ return True
112
+
113
+ return any(fnmatch.fnmatch(path_str, p) for p in self.include_patterns)
114
+
115
+
116
+ class PatternExample(BaseModel):
117
+ """A code example showing the approved or forbidden pattern.
118
+
119
+ Providing concrete examples is critical — agents need to see
120
+ "do this instead" not just "don't do that."
121
+ """
122
+
123
+ model_config = ConfigDict(frozen=True)
124
+
125
+ code: str
126
+ language: Language = Language.UNKNOWN
127
+ description: str = ""
128
+ is_approved: bool = True # True = do this, False = don't do this
129
+
130
+
131
+ class ContractRule(BaseModel):
132
+ """A single rule within a quality contract.
133
+
134
+ Edge cases:
135
+ - Rule with regex pattern: must compile without error at load time
136
+ - Rule with threshold: value must be positive and reasonable
137
+ - Rule with examples: examples should match the rule's language filter
138
+ - Rule in migration mode: has deadline, old pattern is WARNING before
139
+ deadline and ERROR after
140
+ """
141
+
142
+ model_config = ConfigDict(frozen=True)
143
+
144
+ id: str = Field(description="Rule ID within this contract, e.g., 'no-direct-db'")
145
+ name: str = Field(description="Human-readable rule name")
146
+ description: str = Field(description="What this rule checks and why")
147
+ kind: RuleKind
148
+ severity: ContractSeverity = ContractSeverity.WARNING
149
+
150
+ # Detection configuration
151
+ pattern: str | None = Field(
152
+ default=None,
153
+ description="Regex pattern to detect violations (for PATTERN rules)",
154
+ )
155
+ anti_pattern: str | None = Field(
156
+ default=None,
157
+ description="Regex pattern that SHOULD be present (absence = violation)",
158
+ )
159
+ threshold_metric: str | None = Field(
160
+ default=None,
161
+ description="Metric name for THRESHOLD rules: 'max_lines', 'max_complexity', etc.",
162
+ )
163
+ threshold_value: float | None = Field(
164
+ default=None,
165
+ description="Maximum allowed value for the threshold metric",
166
+ )
167
+
168
+ # Context for agents
169
+ examples: list[PatternExample] = Field(
170
+ default_factory=list,
171
+ description="Approved and forbidden code examples",
172
+ )
173
+ fix_suggestion: str | None = Field(
174
+ default=None,
175
+ description="How to fix a violation (shown to agents)",
176
+ )
177
+
178
+ # Migration support
179
+ migration_deadline: datetime | None = Field(
180
+ default=None,
181
+ description="After this date, old pattern becomes an ERROR (gradual migration)",
182
+ )
183
+ replaces_pattern: str | None = Field(
184
+ default=None,
185
+ description="The old pattern being migrated away from",
186
+ )
187
+
188
+ @field_validator("migration_deadline")
189
+ @classmethod
190
+ def ensure_utc(cls, v: datetime | None) -> datetime | None:
191
+ if v is None:
192
+ return None
193
+ if v.tzinfo is None:
194
+ return v.replace(tzinfo=UTC)
195
+ return v.astimezone(UTC)
196
+
197
+ @property
198
+ def effective_severity(self) -> ContractSeverity:
199
+ """Severity adjusted for migration deadlines.
200
+
201
+ Before deadline: original severity (usually WARNING)
202
+ After deadline: ERROR (migration period is over)
203
+ """
204
+ if self.migration_deadline and datetime.now(UTC) > self.migration_deadline:
205
+ return ContractSeverity.ERROR
206
+ return self.severity
207
+
208
+
209
+ class QualityContract(BaseModel):
210
+ """A named collection of quality rules.
211
+
212
+ Contracts are the top-level organizational unit. A project might have:
213
+ - "core-architecture" contract (layer rules)
214
+ - "react-patterns" contract (frontend-specific)
215
+ - "api-conventions" contract (endpoint standards)
216
+ - "ai-guardrails" contract (AI-specific anti-patterns)
217
+
218
+ Each contract has a scope (which files it applies to) and a priority
219
+ (for conflict resolution).
220
+ """
221
+
222
+ model_config = ConfigDict(frozen=True)
223
+
224
+ id: str = Field(description="Contract ID, e.g., 'core-architecture'")
225
+ name: str = Field(description="Human-readable name")
226
+ description: str = Field(description="What this contract enforces and why")
227
+ version: str = Field(default="1.0.0")
228
+ priority: int = Field(
229
+ default=100,
230
+ ge=0,
231
+ le=1000,
232
+ description="Higher priority wins in conflicts. 0=lowest, 1000=highest.",
233
+ )
234
+
235
+ scope: ScopeFilter = Field(default_factory=ScopeFilter)
236
+ rules: list[ContractRule] = Field(default_factory=list)
237
+
238
+ # Metadata
239
+ author: str = Field(default="unknown")
240
+ tags: list[str] = Field(default_factory=list)
241
+ is_builtin: bool = Field(
242
+ default=False,
243
+ description="True for contracts shipped with codebase-intel",
244
+ )
245
+
246
+ def rules_for_file(
247
+ self,
248
+ file_path: Path,
249
+ language: Language = Language.UNKNOWN,
250
+ is_test: bool = False,
251
+ is_generated: bool = False,
252
+ ) -> list[ContractRule]:
253
+ """Get applicable rules for a specific file.
254
+
255
+ Edge case: contract scope matches but individual rules may
256
+ have language-specific patterns. A Python regex won't match
257
+ in a TypeScript file. We return all rules and let the evaluator
258
+ handle language-specific matching.
259
+ """
260
+ if not self.scope.matches(file_path, language, is_test, is_generated):
261
+ return []
262
+ return list(self.rules)
263
+
264
+ def to_context_string(self, verbose: bool = False) -> str:
265
+ """Serialize for inclusion in agent context.
266
+
267
+ Compact mode: rule names and descriptions only.
268
+ Verbose mode: includes examples and fix suggestions.
269
+ """
270
+ lines = [
271
+ f"## Contract: {self.name} [{self.id}]",
272
+ f"Priority: {self.priority} | Rules: {len(self.rules)}",
273
+ f"{self.description}",
274
+ "",
275
+ ]
276
+
277
+ for rule in self.rules:
278
+ severity_badge = {
279
+ ContractSeverity.ERROR: "ERROR",
280
+ ContractSeverity.WARNING: "WARN",
281
+ ContractSeverity.INFO: "INFO",
282
+ }[rule.effective_severity]
283
+
284
+ lines.append(f"- [{severity_badge}] **{rule.name}**: {rule.description}")
285
+
286
+ if verbose and rule.fix_suggestion:
287
+ lines.append(f" Fix: {rule.fix_suggestion}")
288
+
289
+ if verbose and rule.examples:
290
+ for ex in rule.examples:
291
+ label = "DO" if ex.is_approved else "DON'T"
292
+ lines.append(f" {label}: {ex.description}")
293
+
294
+ return "\n".join(lines)
295
+
296
+
297
+ # ---------------------------------------------------------------------------
298
+ # Built-in contract definitions (shipped with the tool)
299
+ # ---------------------------------------------------------------------------
300
+
301
+
302
+ def builtin_ai_guardrails() -> QualityContract:
303
+ """Contract for detecting common AI code generation mistakes.
304
+
305
+ These are patterns that AI agents frequently produce but humans
306
+ wouldn't write. Detecting them before they reach the codebase
307
+ is a key value proposition.
308
+ """
309
+ return QualityContract(
310
+ id="ai-guardrails",
311
+ name="AI Code Generation Guardrails",
312
+ description=(
313
+ "Detects common anti-patterns in AI-generated code: "
314
+ "hallucinated imports, over-abstraction, unnecessary error handling, "
315
+ "verbose comments restating code, and speculative features."
316
+ ),
317
+ priority=500,
318
+ is_builtin=True,
319
+ tags=["ai", "quality", "guardrails"],
320
+ rules=[
321
+ ContractRule(
322
+ id="no-hallucinated-imports",
323
+ name="No hallucinated imports",
324
+ description=(
325
+ "AI agents sometimes import modules that don't exist in the project. "
326
+ "Verify all imports resolve to actual files or installed packages."
327
+ ),
328
+ kind=RuleKind.AI_ANTIPATTERN,
329
+ severity=ContractSeverity.ERROR,
330
+ fix_suggestion="Check if the imported module exists. Use the code graph to verify.",
331
+ ),
332
+ ContractRule(
333
+ id="no-over-abstraction",
334
+ name="No premature abstraction",
335
+ description=(
336
+ "AI tends to create unnecessary base classes, factory patterns, "
337
+ "and utility wrappers for one-time operations. Only abstract when "
338
+ "there are 3+ concrete uses."
339
+ ),
340
+ kind=RuleKind.AI_ANTIPATTERN,
341
+ severity=ContractSeverity.WARNING,
342
+ fix_suggestion="Inline the logic. Create abstractions only when there are 3+ users.",
343
+ ),
344
+ ContractRule(
345
+ id="no-unnecessary-error-handling",
346
+ name="No redundant error handling",
347
+ description=(
348
+ "AI often wraps code in try/except or if-null checks for conditions "
349
+ "that can't occur (e.g., checking if a required field is None after "
350
+ "Pydantic validation). Trust the type system and framework guarantees."
351
+ ),
352
+ kind=RuleKind.AI_ANTIPATTERN,
353
+ severity=ContractSeverity.WARNING,
354
+ fix_suggestion="Remove error handling for impossible conditions. Trust types and validation.",
355
+ ),
356
+ ContractRule(
357
+ id="no-restating-comments",
358
+ name="No comments that restate code",
359
+ description=(
360
+ "AI generates excessive comments like '# Increment counter' above 'counter += 1'. "
361
+ "Comments should explain WHY, never WHAT."
362
+ ),
363
+ kind=RuleKind.AI_ANTIPATTERN,
364
+ severity=ContractSeverity.INFO,
365
+ pattern=r"#\s*(set|get|create|update|delete|increment|initialize|return)\s",
366
+ fix_suggestion="Remove comments that describe what the code does. Only keep WHY comments.",
367
+ ),
368
+ ContractRule(
369
+ id="no-speculative-features",
370
+ name="No unrequested features",
371
+ description=(
372
+ "AI often adds extra configuration, feature flags, or extensibility "
373
+ "points that weren't asked for. YAGNI — You Ain't Gonna Need It."
374
+ ),
375
+ kind=RuleKind.AI_ANTIPATTERN,
376
+ severity=ContractSeverity.WARNING,
377
+ fix_suggestion="Remove features not explicitly requested. Build exactly what was asked.",
378
+ ),
379
+ ContractRule(
380
+ id="no-excessive-logging",
381
+ name="No excessive logging",
382
+ description=(
383
+ "AI often adds a log statement for every operation. Only log at "
384
+ "meaningful boundaries: errors, external calls, state transitions."
385
+ ),
386
+ kind=RuleKind.AI_ANTIPATTERN,
387
+ severity=ContractSeverity.INFO,
388
+ fix_suggestion="Keep logging at meaningful boundaries only. Remove noise logs.",
389
+ ),
390
+ ],
391
+ )
392
+
393
+
394
+ def builtin_architecture_rules() -> QualityContract:
395
+ """Generic architectural rules applicable to most projects."""
396
+ return QualityContract(
397
+ id="architecture-basics",
398
+ name="Basic Architecture Rules",
399
+ description="Fundamental architectural constraints: layer separation, dependency direction, no circular imports.",
400
+ priority=400,
401
+ is_builtin=True,
402
+ tags=["architecture"],
403
+ rules=[
404
+ ContractRule(
405
+ id="no-circular-imports",
406
+ name="No circular import chains",
407
+ description="Circular imports cause initialization issues and indicate tangled architecture.",
408
+ kind=RuleKind.ARCHITECTURAL,
409
+ severity=ContractSeverity.WARNING,
410
+ fix_suggestion="Break the cycle by extracting shared types into a separate module.",
411
+ ),
412
+ ContractRule(
413
+ id="no-god-files",
414
+ name="No files exceeding 500 lines",
415
+ description="Files over 500 lines are hard to navigate and usually need splitting.",
416
+ kind=RuleKind.THRESHOLD,
417
+ severity=ContractSeverity.WARNING,
418
+ threshold_metric="max_lines",
419
+ threshold_value=500,
420
+ fix_suggestion="Split into focused modules by responsibility.",
421
+ ),
422
+ ContractRule(
423
+ id="no-god-functions",
424
+ name="No functions exceeding 50 lines",
425
+ description="Long functions are hard to test and understand. Extract sub-routines.",
426
+ kind=RuleKind.THRESHOLD,
427
+ severity=ContractSeverity.WARNING,
428
+ threshold_metric="max_function_lines",
429
+ threshold_value=50,
430
+ fix_suggestion="Extract helper functions or use early returns to reduce length.",
431
+ ),
432
+ ],
433
+ )
@@ -0,0 +1,225 @@
1
+ """Contract registry — loads, stores, and queries quality contracts.
2
+
3
+ Edge cases:
4
+ - No contracts directory: return builtins only (tool works out of the box)
5
+ - YAML contract file with syntax error: skip file, report error, continue
6
+ - Contract with invalid rule regex: validated at load time, skip bad rules
7
+ - Duplicate contract IDs: warn, keep the project-level one (overrides builtin)
8
+ - Hot reload: detect file changes and reload contracts without restart
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from pathlib import Path
15
+ from typing import TYPE_CHECKING
16
+
17
+ import yaml
18
+
19
+ from codebase_intel.contracts.models import (
20
+ ContractRule,
21
+ PatternExample,
22
+ QualityContract,
23
+ RuleKind,
24
+ ScopeFilter,
25
+ builtin_ai_guardrails,
26
+ builtin_architecture_rules,
27
+ )
28
+ from codebase_intel.core.exceptions import ContractParseError, ErrorContext
29
+ from codebase_intel.core.types import ContractSeverity, Language
30
+
31
+ if TYPE_CHECKING:
32
+ from codebase_intel.core.config import ContractConfig
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class ContractRegistry:
38
+ """Manages loading and querying quality contracts."""
39
+
40
+ def __init__(self, config: ContractConfig, project_root: Path) -> None:
41
+ self._config = config
42
+ self._project_root = project_root
43
+ self._contracts: dict[str, QualityContract] = {}
44
+ self._loaded = False
45
+
46
+ def load(self) -> None:
47
+ """Load all contracts from builtins and project directory.
48
+
49
+ Load order:
50
+ 1. Built-in contracts (shipped with codebase-intel)
51
+ 2. Project contracts (from .codebase-intel/contracts/)
52
+
53
+ Project contracts override builtins with the same ID.
54
+ """
55
+ self._contracts.clear()
56
+
57
+ # Load builtins
58
+ if self._config.enable_builtin_contracts:
59
+ for builtin in self._get_builtins():
60
+ self._contracts[builtin.id] = builtin
61
+
62
+ # Load project contracts
63
+ contracts_dir = self._config.contracts_dir
64
+ if contracts_dir.exists():
65
+ for yaml_file in sorted(contracts_dir.glob("*.yaml")):
66
+ try:
67
+ contract = self._load_contract_file(yaml_file)
68
+ if contract.id in self._contracts and self._contracts[contract.id].is_builtin:
69
+ logger.info(
70
+ "Project contract '%s' overrides builtin",
71
+ contract.id,
72
+ )
73
+ self._contracts[contract.id] = contract
74
+ except Exception as exc:
75
+ logger.warning(
76
+ "Failed to load contract %s: %s", yaml_file, exc
77
+ )
78
+
79
+ self._loaded = True
80
+ logger.info(
81
+ "Loaded %d contracts (%d builtin, %d project)",
82
+ len(self._contracts),
83
+ sum(1 for c in self._contracts.values() if c.is_builtin),
84
+ sum(1 for c in self._contracts.values() if not c.is_builtin),
85
+ )
86
+
87
+ def get_all(self) -> list[QualityContract]:
88
+ """Get all loaded contracts, sorted by priority (highest first)."""
89
+ if not self._loaded:
90
+ self.load()
91
+ return sorted(
92
+ self._contracts.values(),
93
+ key=lambda c: c.priority,
94
+ reverse=True,
95
+ )
96
+
97
+ def get(self, contract_id: str) -> QualityContract | None:
98
+ """Get a specific contract by ID."""
99
+ if not self._loaded:
100
+ self.load()
101
+ return self._contracts.get(contract_id)
102
+
103
+ def get_for_file(self, file_path: Path, language: Language = Language.UNKNOWN) -> list[QualityContract]:
104
+ """Get all contracts applicable to a specific file."""
105
+ if not self._loaded:
106
+ self.load()
107
+ return [
108
+ c for c in self._contracts.values()
109
+ if c.scope.matches(file_path, language)
110
+ ]
111
+
112
+ def _get_builtins(self) -> list[QualityContract]:
113
+ """Get all built-in contract definitions."""
114
+ return [
115
+ builtin_ai_guardrails(),
116
+ builtin_architecture_rules(),
117
+ ]
118
+
119
+ def _load_contract_file(self, yaml_file: Path) -> QualityContract:
120
+ """Load a contract from a YAML file.
121
+
122
+ Expected format:
123
+ ```yaml
124
+ id: my-contract
125
+ name: My Quality Contract
126
+ description: What this enforces
127
+ priority: 200
128
+ scope:
129
+ include_patterns: ["src/api/**"]
130
+ languages: ["python"]
131
+ rules:
132
+ - id: no-raw-sql
133
+ name: No raw SQL queries
134
+ kind: architectural
135
+ severity: error
136
+ pattern: "execute\\(.*SELECT|INSERT|UPDATE|DELETE"
137
+ fix_suggestion: Use the repository pattern
138
+ ```
139
+
140
+ Edge case: partial YAML (missing optional fields) → Pydantic defaults fill in.
141
+ Edge case: unknown fields → ignored (forward compatibility).
142
+ """
143
+ content = yaml_file.read_text(encoding="utf-8")
144
+ try:
145
+ data = yaml.safe_load(content)
146
+ except yaml.YAMLError as exc:
147
+ raise ContractParseError(
148
+ f"Invalid YAML in {yaml_file.name}: {exc}",
149
+ ErrorContext(file_path=yaml_file),
150
+ ) from exc
151
+
152
+ if not isinstance(data, dict):
153
+ raise ContractParseError(
154
+ f"Contract file {yaml_file.name} must be a YAML mapping",
155
+ ErrorContext(file_path=yaml_file),
156
+ )
157
+
158
+ # Parse scope
159
+ scope_data = data.get("scope", {})
160
+ if isinstance(scope_data, dict):
161
+ # Convert string language names to Language enum
162
+ if "languages" in scope_data:
163
+ scope_data["languages"] = [
164
+ Language(l) if isinstance(l, str) else l
165
+ for l in scope_data["languages"]
166
+ ]
167
+ data["scope"] = ScopeFilter(**scope_data)
168
+
169
+ # Parse rules
170
+ rules = []
171
+ for rule_data in data.get("rules", []):
172
+ if isinstance(rule_data, dict):
173
+ # Convert string enums
174
+ if "kind" in rule_data:
175
+ rule_data["kind"] = RuleKind(rule_data["kind"])
176
+ if "severity" in rule_data:
177
+ rule_data["severity"] = ContractSeverity(rule_data["severity"])
178
+
179
+ # Parse examples
180
+ examples = []
181
+ for ex in rule_data.get("examples", []):
182
+ if isinstance(ex, dict):
183
+ examples.append(PatternExample(**ex))
184
+ rule_data["examples"] = examples
185
+
186
+ rules.append(ContractRule(**rule_data))
187
+ data["rules"] = rules
188
+
189
+ return QualityContract(**data)
190
+
191
+ async def create_template(self, contract_id: str, name: str) -> Path:
192
+ """Create a template contract file for the user to customize.
193
+
194
+ This is called by `codebase-intel init` to bootstrap project contracts.
195
+ """
196
+ self._config.contracts_dir.mkdir(parents=True, exist_ok=True)
197
+
198
+ template = {
199
+ "id": contract_id,
200
+ "name": name,
201
+ "description": f"Quality rules for {name}",
202
+ "priority": 200,
203
+ "scope": {
204
+ "include_patterns": ["src/**"],
205
+ "exclude_patterns": ["node_modules/**", "dist/**"],
206
+ },
207
+ "rules": [
208
+ {
209
+ "id": "example-rule",
210
+ "name": "Example Rule",
211
+ "description": "Replace this with your actual rule",
212
+ "kind": "pattern",
213
+ "severity": "warning",
214
+ "pattern": "TODO|FIXME|HACK",
215
+ "fix_suggestion": "Resolve the TODO before merging",
216
+ },
217
+ ],
218
+ }
219
+
220
+ file_path = self._config.contracts_dir / f"{contract_id}.yaml"
221
+ file_path.write_text(
222
+ yaml.dump(template, default_flow_style=False, sort_keys=False),
223
+ encoding="utf-8",
224
+ )
225
+ return file_path
@@ -0,0 +1 @@
1
+ """Core types, configuration, and exception hierarchy."""