codedna-analyzer 1.4.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 (37) hide show
  1. codedna/__init__.py +3 -0
  2. codedna/analyzers/__init__.py +1 -0
  3. codedna/analyzers/ai_analyzer.py +111 -0
  4. codedna/analyzers/architecture_detector.py +150 -0
  5. codedna/analyzers/cache_manager.py +70 -0
  6. codedna/analyzers/code_smell_detector.py +302 -0
  7. codedna/analyzers/complexity_analyzer.py +329 -0
  8. codedna/analyzers/dependency_mapper.py +171 -0
  9. codedna/analyzers/developer_analyzer.py +148 -0
  10. codedna/analyzers/diff_engine.py +395 -0
  11. codedna/analyzers/dna_generator.py +308 -0
  12. codedna/analyzers/evolution_engine.py +193 -0
  13. codedna/analyzers/github_analyzer.py +86 -0
  14. codedna/analyzers/language_detector.py +151 -0
  15. codedna/analyzers/repo_cloner.py +91 -0
  16. codedna/analyzers/security_detector.py +271 -0
  17. codedna/analyzers/structure_analyzer.py +91 -0
  18. codedna/ci/__init__.py +5 -0
  19. codedna/ci/gatekeeper.py +421 -0
  20. codedna/cli.py +839 -0
  21. codedna/compliance/__init__.py +5 -0
  22. codedna/compliance/sbom.py +410 -0
  23. codedna/config.py +167 -0
  24. codedna/mcp/__init__.py +5 -0
  25. codedna/mcp/protocol.py +65 -0
  26. codedna/mcp/server.py +393 -0
  27. codedna/remediation/__init__.py +5 -0
  28. codedna/remediation/planner.py +244 -0
  29. codedna/visualization/__init__.py +1 -0
  30. codedna/visualization/html_export.py +507 -0
  31. codedna/visualization/renderer.py +373 -0
  32. codedna_analyzer-1.4.0.dist-info/METADATA +425 -0
  33. codedna_analyzer-1.4.0.dist-info/RECORD +37 -0
  34. codedna_analyzer-1.4.0.dist-info/WHEEL +5 -0
  35. codedna_analyzer-1.4.0.dist-info/entry_points.txt +2 -0
  36. codedna_analyzer-1.4.0.dist-info/licenses/LICENSE +21 -0
  37. codedna_analyzer-1.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ """Compliance and Software Bill of Materials (SBOM) Subsystem."""
2
+
3
+ from .sbom import SBOMGenerator
4
+
5
+ __all__ = ["SBOMGenerator"]
@@ -0,0 +1,410 @@
1
+ """Software Bill of Materials (SBOM) Engine for CodeDNA.
2
+
3
+ Discovers multi-ecosystem project dependencies and generates industry-standard
4
+ CycloneDX (v1.5 JSON) and SPDX (v2.3 JSON) software manifests.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ import os
12
+ import re
13
+ import uuid
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ IGNORE_DIRS = {
21
+ ".git", ".svn", ".hg", "node_modules", "venv", ".venv", "env",
22
+ "__pycache__", ".pytest_cache", "build", "dist", "target", "vendor"
23
+ }
24
+
25
+
26
+ class SBOMGenerator:
27
+ """Discovers project dependencies and exports CycloneDX / SPDX SBOM specifications."""
28
+
29
+ def __init__(self, repo_path: str | Path = ".") -> None:
30
+ self.repo_path = Path(repo_path).resolve()
31
+
32
+ def discover_dependencies(self) -> List[Dict[str, Any]]:
33
+ """Scans workspace manifests across Python, Node.js, Rust, and Go ecosystems."""
34
+ dependencies: List[Dict[str, Any]] = []
35
+ seen_keys = set()
36
+
37
+ for root, dirs, files in os.walk(self.repo_path):
38
+ dirs[:] = [d for d in dirs if d not in IGNORE_DIRS and not d.startswith(".")]
39
+
40
+ # 1. Python: pyproject.toml
41
+ if "pyproject.toml" in files:
42
+ pyproject_path = Path(root) / "pyproject.toml"
43
+ deps = self._parse_pyproject(pyproject_path)
44
+ for d in deps:
45
+ key = (d["name"], d.get("version", "*"), d["ecosystem"])
46
+ if key not in seen_keys:
47
+ seen_keys.add(key)
48
+ dependencies.append(d)
49
+
50
+ # 2. Python: requirements.txt
51
+ for f in files:
52
+ if f == "requirements.txt" or f.endswith(".requirements.txt"):
53
+ req_path = Path(root) / f
54
+ deps = self._parse_requirements_txt(req_path)
55
+ for d in deps:
56
+ key = (d["name"], d.get("version", "*"), d["ecosystem"])
57
+ if key not in seen_keys:
58
+ seen_keys.add(key)
59
+ dependencies.append(d)
60
+
61
+ # 3. Node.js: package.json & package-lock.json
62
+ if "package.json" in files:
63
+ pkg_path = Path(root) / "package.json"
64
+ lock_path = Path(root) / "package-lock.json" if "package-lock.json" in files else None
65
+ deps = self._parse_package_json(pkg_path, lock_path)
66
+ for d in deps:
67
+ key = (d["name"], d.get("version", "*"), d["ecosystem"])
68
+ if key not in seen_keys:
69
+ seen_keys.add(key)
70
+ dependencies.append(d)
71
+
72
+ # 4. Rust: Cargo.toml & Cargo.lock
73
+ if "Cargo.toml" in files:
74
+ cargo_path = Path(root) / "Cargo.toml"
75
+ cargo_lock = Path(root) / "Cargo.lock" if "Cargo.lock" in files else None
76
+ deps = self._parse_cargo_toml(cargo_path, cargo_lock)
77
+ for d in deps:
78
+ key = (d["name"], d.get("version", "*"), d["ecosystem"])
79
+ if key not in seen_keys:
80
+ seen_keys.add(key)
81
+ dependencies.append(d)
82
+
83
+ # 5. Go: go.mod
84
+ if "go.mod" in files:
85
+ go_mod_path = Path(root) / "go.mod"
86
+ deps = self._parse_go_mod(go_mod_path)
87
+ for d in deps:
88
+ key = (d["name"], d.get("version", "*"), d["ecosystem"])
89
+ if key not in seen_keys:
90
+ seen_keys.add(key)
91
+ dependencies.append(d)
92
+
93
+ dependencies.sort(key=lambda x: (x["ecosystem"], x["name"]))
94
+ return dependencies
95
+
96
+ def _parse_pyproject(self, path: Path) -> List[Dict[str, Any]]:
97
+ results = []
98
+ try:
99
+ content = path.read_text(encoding="utf-8", errors="ignore")
100
+ # 1. Standard PEP 621 dependencies = [...]
101
+ pep621_match = re.search(r"dependencies\s*=\s*\[(.*?)\]", content, re.DOTALL)
102
+ if pep621_match:
103
+ raw_deps = pep621_match.group(1)
104
+ for item in re.finditer(r'["\']([A-Za-z0-9_.\-]+)(?:([><=~!^]+.*?)?)?["\']', raw_deps):
105
+ name = item.group(1).strip()
106
+ ver = item.group(2).strip() if item.group(2) else "*"
107
+ results.append({
108
+ "name": name,
109
+ "version": ver,
110
+ "ecosystem": "pypi",
111
+ "manifest": str(path.relative_to(self.repo_path)),
112
+ "scope": "required",
113
+ "purl": f"pkg:pypi/{name.lower()}@{ver}" if ver != "*" else f"pkg:pypi/{name.lower()}"
114
+ })
115
+
116
+ # 2. Poetry tool.poetry.dependencies
117
+ poetry_match = re.search(r"\[tool\.poetry\.dependencies\](.*?)(?:\[|\Z)", content, re.DOTALL)
118
+ if poetry_match:
119
+ for line in poetry_match.group(1).splitlines():
120
+ line = line.strip()
121
+ if not line or line.startswith("#") or line.startswith("python"):
122
+ continue
123
+ m = re.match(r'([A-Za-z0-9_.\-]+)\s*=\s*["\']?([^"\']+)["\']?', line)
124
+ if m:
125
+ name = m.group(1).strip()
126
+ ver = m.group(2).strip()
127
+ results.append({
128
+ "name": name,
129
+ "version": ver,
130
+ "ecosystem": "pypi",
131
+ "manifest": str(path.relative_to(self.repo_path)),
132
+ "scope": "required",
133
+ "purl": f"pkg:pypi/{name.lower()}@{ver}"
134
+ })
135
+ except Exception as e:
136
+ logger.debug("Failed to parse pyproject.toml at %s: %s", path, e)
137
+ return results
138
+
139
+ def _parse_requirements_txt(self, path: Path) -> List[Dict[str, Any]]:
140
+ results = []
141
+ try:
142
+ content = path.read_text(encoding="utf-8", errors="ignore")
143
+ for line in content.splitlines():
144
+ line = line.strip()
145
+ if not line or line.startswith("#") or line.startswith("-"):
146
+ continue
147
+ m = re.match(r"^([A-Za-z0-9_.\-]+)(?:([><=~!^]+[A-Za-z0-9_.\-+]+))?", line)
148
+ if m:
149
+ name = m.group(1).strip()
150
+ ver = m.group(2).strip() if m.group(2) else "*"
151
+ results.append({
152
+ "name": name,
153
+ "version": ver,
154
+ "ecosystem": "pypi",
155
+ "manifest": str(path.relative_to(self.repo_path)),
156
+ "scope": "required",
157
+ "purl": f"pkg:pypi/{name.lower()}@{ver}" if ver != "*" else f"pkg:pypi/{name.lower()}"
158
+ })
159
+ except Exception as e:
160
+ logger.debug("Failed to parse requirements.txt at %s: %s", path, e)
161
+ return results
162
+
163
+ def _parse_package_json(self, path: Path, lock_path: Optional[Path] = None) -> List[Dict[str, Any]]:
164
+ results = []
165
+ locked_versions: Dict[str, str] = {}
166
+ if lock_path and lock_path.exists():
167
+ try:
168
+ lock_data = json.loads(lock_path.read_text(encoding="utf-8", errors="ignore"))
169
+ packages = lock_data.get("packages", {})
170
+ for pkg_key, pkg_info in packages.items():
171
+ name = pkg_key.replace("node_modules/", "")
172
+ if name and "version" in pkg_info:
173
+ locked_versions[name] = pkg_info["version"]
174
+ for name, info in lock_data.get("dependencies", {}).items():
175
+ if "version" in info:
176
+ locked_versions[name] = info["version"]
177
+ except Exception as e:
178
+ logger.debug("Failed to parse package-lock.json: %s", e)
179
+
180
+ try:
181
+ data = json.loads(path.read_text(encoding="utf-8", errors="ignore"))
182
+ manifest_str = str(path.relative_to(self.repo_path))
183
+
184
+ # Runtime dependencies
185
+ for name, ver in data.get("dependencies", {}).items():
186
+ resolved_ver = locked_versions.get(name, str(ver))
187
+ results.append({
188
+ "name": name,
189
+ "version": resolved_ver,
190
+ "declared_version": str(ver),
191
+ "ecosystem": "npm",
192
+ "manifest": manifest_str,
193
+ "scope": "required",
194
+ "purl": f"pkg:npm/{name}@{resolved_ver}"
195
+ })
196
+
197
+ # Dev dependencies
198
+ for name, ver in data.get("devDependencies", {}).items():
199
+ resolved_ver = locked_versions.get(name, str(ver))
200
+ results.append({
201
+ "name": name,
202
+ "version": resolved_ver,
203
+ "declared_version": str(ver),
204
+ "ecosystem": "npm",
205
+ "manifest": manifest_str,
206
+ "scope": "optional",
207
+ "purl": f"pkg:npm/{name}@{resolved_ver}"
208
+ })
209
+ except Exception as e:
210
+ logger.debug("Failed to parse package.json at %s: %s", path, e)
211
+ return results
212
+
213
+ def _parse_cargo_toml(self, path: Path, lock_path: Optional[Path] = None) -> List[Dict[str, Any]]:
214
+ results = []
215
+ try:
216
+ content = path.read_text(encoding="utf-8", errors="ignore")
217
+ in_deps = False
218
+ for line in content.splitlines():
219
+ line = line.strip()
220
+ if line.startswith("[dependencies]") or line.startswith("[dev-dependencies]"):
221
+ in_deps = True
222
+ continue
223
+ if line.startswith("[") and in_deps:
224
+ in_deps = False
225
+ continue
226
+ if in_deps and "=" in line and not line.startswith("#"):
227
+ name, val = line.split("=", 1)
228
+ name = name.strip()
229
+ val = val.strip().strip('"\'')
230
+ ver = val
231
+ if "version" in val:
232
+ m = re.search(r'version\s*=\s*["\']([^"\']+)["\']', val)
233
+ if m:
234
+ ver = m.group(1)
235
+ results.append({
236
+ "name": name,
237
+ "version": ver,
238
+ "ecosystem": "cargo",
239
+ "manifest": str(path.relative_to(self.repo_path)),
240
+ "scope": "required",
241
+ "purl": f"pkg:cargo/{name}@{ver}"
242
+ })
243
+ except Exception as e:
244
+ logger.debug("Failed to parse Cargo.toml at %s: %s", path, e)
245
+ return results
246
+
247
+ def _parse_go_mod(self, path: Path) -> List[Dict[str, Any]]:
248
+ results = []
249
+ try:
250
+ content = path.read_text(encoding="utf-8", errors="ignore")
251
+ in_require = False
252
+ for line in content.splitlines():
253
+ line = line.strip()
254
+ if line.startswith("require ("):
255
+ in_require = True
256
+ continue
257
+ if in_require and line == ")":
258
+ in_require = False
259
+ continue
260
+ if in_require and line and not line.startswith("//"):
261
+ parts = line.split()
262
+ if len(parts) >= 2:
263
+ name = parts[0]
264
+ ver = parts[1]
265
+ results.append({
266
+ "name": name,
267
+ "version": ver,
268
+ "ecosystem": "golang",
269
+ "manifest": str(path.relative_to(self.repo_path)),
270
+ "scope": "required",
271
+ "purl": f"pkg:golang/{name}@{ver}"
272
+ })
273
+ elif line.startswith("require ") and not in_require:
274
+ parts = line.replace("require ", "").split()
275
+ if len(parts) >= 2:
276
+ name = parts[0]
277
+ ver = parts[1]
278
+ results.append({
279
+ "name": name,
280
+ "version": ver,
281
+ "ecosystem": "golang",
282
+ "manifest": str(path.relative_to(self.repo_path)),
283
+ "scope": "required",
284
+ "purl": f"pkg:golang/{name}@{ver}"
285
+ })
286
+ except Exception as e:
287
+ logger.debug("Failed to parse go.mod at %s: %s", path, e)
288
+ return results
289
+
290
+ def generate_cyclonedx_json(self) -> Dict[str, Any]:
291
+ """Generates an OASIS / OWASP CycloneDX v1.5 JSON standard document."""
292
+ deps = self.discover_dependencies()
293
+ now = datetime.now(timezone.utc).isoformat()
294
+ root_name = self.repo_path.name or "application"
295
+
296
+ components = []
297
+ dependencies_refs = []
298
+
299
+ for d in deps:
300
+ comp_id = f"{d['ecosystem']}-{d['name']}-{d['version']}".replace("/", "-")
301
+ comp = {
302
+ "bom-ref": comp_id,
303
+ "type": "library",
304
+ "name": d["name"],
305
+ "version": d["version"],
306
+ "purl": d["purl"],
307
+ "scope": d["scope"],
308
+ "properties": [
309
+ {"name": "codedna:ecosystem", "value": d["ecosystem"]},
310
+ {"name": "codedna:manifest", "value": d["manifest"]}
311
+ ]
312
+ }
313
+ components.append(comp)
314
+ dependencies_refs.append(comp_id)
315
+
316
+ doc = {
317
+ "bomFormat": "CycloneDX",
318
+ "specVersion": "1.5",
319
+ "serialNumber": f"urn:uuid:{uuid.uuid4()}",
320
+ "version": 1,
321
+ "metadata": {
322
+ "timestamp": now,
323
+ "tools": [
324
+ {
325
+ "vendor": "CodeDNA",
326
+ "name": "codedna-sbom-engine",
327
+ "version": "1.4.0"
328
+ }
329
+ ],
330
+ "component": {
331
+ "type": "application",
332
+ "name": root_name,
333
+ "version": "latest",
334
+ "bom-ref": f"app-{root_name}"
335
+ }
336
+ },
337
+ "components": components,
338
+ "dependencies": [
339
+ {
340
+ "ref": f"app-{root_name}",
341
+ "dependsOn": dependencies_refs
342
+ }
343
+ ]
344
+ }
345
+ return doc
346
+
347
+ def generate_spdx_json(self) -> Dict[str, Any]:
348
+ """Generates a Linux Foundation SPDX v2.3 JSON specification document."""
349
+ deps = self.discover_dependencies()
350
+ now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
351
+ doc_name = self.repo_path.name or "codedna-project"
352
+
353
+ packages = [
354
+ {
355
+ "name": doc_name,
356
+ "SPDXID": "SPDXRef-RootPackage",
357
+ "versionInfo": "1.0.0",
358
+ "downloadLocation": "NOASSERTION",
359
+ "filesAnalyzed": False,
360
+ "licenseConcluded": "NOASSERTION",
361
+ "licenseDeclared": "NOASSERTION"
362
+ }
363
+ ]
364
+ relationships = [
365
+ {
366
+ "spdxElementId": "SPDXRef-DOCUMENT",
367
+ "relatedSpdxElement": "SPDXRef-RootPackage",
368
+ "relationshipType": "DESCRIBES"
369
+ }
370
+ ]
371
+
372
+ for idx, d in enumerate(deps, start=1):
373
+ spdx_id = f"SPDXRef-Package-{idx}-{re.sub(r'[^A-Za-z0-9.-]', '-', d['name'])}"
374
+ pkg = {
375
+ "name": d["name"],
376
+ "SPDXID": spdx_id,
377
+ "versionInfo": d["version"],
378
+ "downloadLocation": "NOASSERTION",
379
+ "filesAnalyzed": False,
380
+ "licenseConcluded": "NOASSERTION",
381
+ "licenseDeclared": "NOASSERTION",
382
+ "externalRefs": [
383
+ {
384
+ "referenceCategory": "PACKAGE-MANAGER",
385
+ "referenceType": "purl",
386
+ "referenceLocator": d["purl"]
387
+ }
388
+ ]
389
+ }
390
+ packages.append(pkg)
391
+ relationships.append({
392
+ "spdxElementId": "SPDXRef-RootPackage",
393
+ "relatedSpdxElement": spdx_id,
394
+ "relationshipType": "DEPENDS_ON"
395
+ })
396
+
397
+ doc = {
398
+ "spdxVersion": "SPDX-2.3",
399
+ "dataLicense": "CC0-1.0",
400
+ "SPDXID": "SPDXRef-DOCUMENT",
401
+ "name": f"{doc_name}-SBOM",
402
+ "documentNamespace": f"https://spdx.org/spdxdocs/{doc_name}-{uuid.uuid4()}",
403
+ "creationInfo": {
404
+ "creators": ["Tool: CodeDNA-1.4.0", "Organization: CodeDNA Intelligence"],
405
+ "created": now
406
+ },
407
+ "packages": packages,
408
+ "relationships": relationships
409
+ }
410
+ return doc
codedna/config.py ADDED
@@ -0,0 +1,167 @@
1
+ """CodeDNA Configuration Loader — supports codedna.toml and pyproject.toml [tool.codedna]."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import fnmatch
6
+ import tomllib
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ @dataclass
13
+ class CodeDNAConfig:
14
+ """Configuration options for CodeDNA analysis, security, and CI quality gates."""
15
+
16
+ # Gatekeeper policies
17
+ min_health: str = "Fair"
18
+ max_critical_smells: int = 0
19
+ max_smells: int | None = None
20
+ fail_on_secrets: bool = True
21
+ fail_on_circular: bool = False
22
+ max_density: float | None = None
23
+ max_drift: float = 20.0
24
+ max_cyclomatic_complexity: int | None = None
25
+ min_maintainability_index: float | None = None
26
+
27
+ # Path exclusions
28
+ ignore_paths: list[str] = field(default_factory=list)
29
+
30
+ # Security whitelisting
31
+ whitelisted_secrets: list[str] = field(default_factory=list)
32
+
33
+ # Smell thresholds
34
+ max_file_lines: int = 500
35
+ max_function_lines: int = 80
36
+ god_class_methods: int = 15
37
+ large_module_files: int = 20
38
+
39
+ @property
40
+ def gate(self) -> CodeDNAConfig:
41
+ """Alias to self for structured access to gatekeeper configuration."""
42
+ return self
43
+
44
+ @property
45
+ def smells(self) -> CodeDNAConfig:
46
+ """Alias to self for structured access to smell thresholds."""
47
+ return self
48
+
49
+ def is_path_ignored(self, rel_path: str) -> bool:
50
+ """Check if relative file path matches any ignore glob patterns."""
51
+ normalized = rel_path.replace("\\", "/").strip("/")
52
+ for pattern in self.ignore_paths:
53
+ norm_pattern = pattern.replace("\\", "/").strip("/")
54
+ clean_base = norm_pattern.rstrip("/*")
55
+ if (
56
+ normalized == clean_base
57
+ or normalized.startswith(clean_base + "/")
58
+ or fnmatch.fnmatch(normalized, norm_pattern)
59
+ or fnmatch.fnmatch(Path(normalized).name, norm_pattern)
60
+ ):
61
+ return True
62
+ return False
63
+
64
+ def is_secret_whitelisted(self, secret_text: str) -> bool:
65
+ """Check if detected secret matches any whitelisted keywords or tokens."""
66
+ clean = secret_text.strip()
67
+ for allowed in self.whitelisted_secrets:
68
+ if allowed in clean or fnmatch.fnmatch(clean, allowed):
69
+ return True
70
+ return False
71
+
72
+
73
+ def load_config(root_path: Path | str) -> CodeDNAConfig:
74
+ """Load configuration from codedna.toml, .codedna.toml, or pyproject.toml."""
75
+ root = Path(root_path)
76
+ candidates = [
77
+ root / "codedna.toml",
78
+ root / ".codedna.toml",
79
+ root / "pyproject.toml",
80
+ ]
81
+
82
+ for candidate in candidates:
83
+ if candidate.is_file():
84
+ try:
85
+ content = candidate.read_bytes()
86
+ data = tomllib.loads(content.decode("utf-8", errors="replace"))
87
+
88
+ section: dict[str, Any] = {}
89
+ if candidate.name == "pyproject.toml":
90
+ section = data.get("tool", {}).get("codedna", {})
91
+ else:
92
+ section = data.get("codedna", data)
93
+
94
+ if not section and candidate.name == "pyproject.toml":
95
+ continue
96
+
97
+ smell_cfg = section.get("smells", {})
98
+ gate_cfg = section.get("gate", section)
99
+
100
+ return CodeDNAConfig(
101
+ min_health=str(gate_cfg.get("min_health", section.get("min_health", "Fair"))),
102
+ max_critical_smells=int(gate_cfg.get("max_critical_smells", section.get("max_critical_smells", 0))),
103
+ max_smells=int(gate_cfg["max_smells"]) if "max_smells" in gate_cfg and gate_cfg["max_smells"] is not None else None,
104
+ fail_on_secrets=bool(gate_cfg.get("fail_on_secrets", section.get("fail_on_secrets", True))),
105
+ fail_on_circular=bool(gate_cfg.get("fail_on_circular", section.get("fail_on_circular", False))),
106
+ max_density=float(gate_cfg["max_density"]) if "max_density" in gate_cfg and gate_cfg["max_density"] is not None else None,
107
+ max_drift=float(gate_cfg.get("max_drift", section.get("max_drift", 20.0))),
108
+ max_cyclomatic_complexity=int(gate_cfg["max_cyclomatic_complexity"]) if "max_cyclomatic_complexity" in gate_cfg and gate_cfg["max_cyclomatic_complexity"] is not None else None,
109
+ min_maintainability_index=float(gate_cfg["min_maintainability_index"]) if "min_maintainability_index" in gate_cfg and gate_cfg["min_maintainability_index"] is not None else None,
110
+ ignore_paths=list(section.get("ignore_paths", [])),
111
+ whitelisted_secrets=list(section.get("whitelisted_secrets", [])),
112
+ max_file_lines=int(smell_cfg.get("max_file_lines", section.get("max_file_lines", 500))),
113
+ max_function_lines=int(smell_cfg.get("max_function_lines", section.get("max_function_lines", 80))),
114
+ god_class_methods=int(smell_cfg.get("god_class_methods", section.get("god_class_methods", 15))),
115
+ large_module_files=int(smell_cfg.get("large_module_files", section.get("large_module_files", 20))),
116
+ )
117
+ except Exception:
118
+ continue
119
+
120
+ return CodeDNAConfig()
121
+
122
+
123
+ def generate_default_config_toml() -> str:
124
+ """Generate default starter codedna.toml file content."""
125
+ return '''# CodeDNA Configuration File (codedna.toml)
126
+ # https://github.com/shenald-dev/codedna
127
+
128
+ # Paths to exclude from analysis, smell checks, and security scans
129
+ ignore_paths = [
130
+ "tests/*",
131
+ "docs/*",
132
+ "build/*",
133
+ "dist/*",
134
+ "*.min.js",
135
+ ]
136
+
137
+ # Whitelist false-positive tokens or development test fixtures
138
+ whitelisted_secrets = [
139
+ "PUBLIC_KEY",
140
+ "EXAMPLE_KEY",
141
+ ]
142
+
143
+ [gate]
144
+ # Minimum acceptable health rating ("Healthy", "Fair", "Needs Attention", "Critical")
145
+ min_health = "Fair"
146
+
147
+ # Maximum allowable critical smells (0 enforces zero-tolerance)
148
+ max_critical_smells = 0
149
+
150
+ # Maximum allowable total smells (omit or leave empty for no cap)
151
+ # max_smells = 50
152
+
153
+ # Fail CI immediately if secrets/credentials or circular dependencies are found
154
+ fail_on_secrets = true
155
+ fail_on_circular = false
156
+
157
+ # Maximum acceptable genetic drift percentage during drift regression gates (0-100)
158
+ max_drift = 20.0
159
+
160
+ [smells]
161
+ # Thresholds for architectural smell detection
162
+ max_file_lines = 500
163
+ max_function_lines = 80
164
+ god_class_methods = 15
165
+ large_module_files = 20
166
+ '''
167
+
@@ -0,0 +1,5 @@
1
+ """CodeDNA Model Context Protocol (MCP 2024-11-05) Gateway."""
2
+
3
+ from .server import CodeDNAMCPServer
4
+
5
+ __all__ = ["CodeDNAMCPServer"]
@@ -0,0 +1,65 @@
1
+ """Model Context Protocol (MCP) JSON-RPC 2.0 schemas and protocol primitives."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass, field
6
+ from typing import Any
7
+
8
+
9
+ @dataclass
10
+ class MCPToolDefinition:
11
+ """Definition of an exposed MCP tool with JSON Schema parameters."""
12
+
13
+ name: str
14
+ description: str
15
+ inputSchema: dict[str, Any] # noqa: N815
16
+
17
+ def to_dict(self) -> dict[str, Any]:
18
+ return asdict(self)
19
+
20
+
21
+ @dataclass
22
+ class JSONRPCRequest:
23
+ """Standard JSON-RPC 2.0 request payload."""
24
+
25
+ jsonrpc: str
26
+ id: str | int | None
27
+ method: str
28
+ params: dict[str, Any] = field(default_factory=dict)
29
+
30
+ @classmethod
31
+ def from_dict(cls, data: Any) -> JSONRPCRequest:
32
+ if not isinstance(data, dict):
33
+ return cls(jsonrpc="2.0", id=None, method="", params={})
34
+ raw_params = data.get("params")
35
+ params = raw_params if isinstance(raw_params, dict) else {}
36
+ return cls(
37
+ jsonrpc=str(data.get("jsonrpc", "2.0")),
38
+ id=data.get("id"),
39
+ method=str(data.get("method", "")),
40
+ params=params,
41
+ )
42
+
43
+
44
+ @dataclass
45
+ class JSONRPCResponse:
46
+ """Standard JSON-RPC 2.0 response payload."""
47
+
48
+ jsonrpc: str = "2.0"
49
+ id: str | int | None = None
50
+ result: Any = None
51
+ error: dict[str, Any] | None = None
52
+
53
+ def to_dict(self) -> dict[str, Any]:
54
+ data: dict[str, Any] = {"jsonrpc": self.jsonrpc, "id": self.id}
55
+ if self.error is not None:
56
+ data["error"] = self.error
57
+ else:
58
+ data["result"] = self.result
59
+ return data
60
+
61
+
62
+ # Standard Error Codes
63
+
64
+
65
+ METHOD_NOT_FOUND = -32601