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,395 @@
1
+ """DNA Diff Engine — compares two CodeDNA profiles and computes genetic drift."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ from datetime import datetime
8
+
9
+
10
+ class DiffEngine:
11
+ """Computes genetic drift, architectural deltas, and similarity between two codebases."""
12
+
13
+ HEALTH_RANKS = {
14
+ "Healthy": 4,
15
+ "Fair": 3,
16
+ "Needs Attention": 2,
17
+ "Critical": 1,
18
+ "Unknown": 0,
19
+ }
20
+
21
+ def compare(self, profile_a: dict, profile_b: dict) -> dict:
22
+ """Compare two DNA profiles and compute comprehensive structural and architectural diff.
23
+
24
+ Args:
25
+ profile_a: Baseline DNA profile dict.
26
+ profile_b: Comparison / target DNA profile dict.
27
+
28
+ Returns:
29
+ Dictionary with similarity score, metric deltas, and mutation verdict.
30
+ """
31
+ source_a = profile_a.get("metadata", {}).get("source", "Profile A")
32
+ source_b = profile_b.get("metadata", {}).get("source", "Profile B")
33
+
34
+ # 1. Calculate similarity score (0.0 to 100.0)
35
+ similarity_score = self.compute_similarity(profile_a, profile_b)
36
+
37
+ # 2. Structural Deltas
38
+ struct_a = profile_a.get("structure_stats", {})
39
+ struct_b = profile_b.get("structure_stats", {})
40
+ structure_delta = {
41
+ "files": struct_b.get("total_files", 0) - struct_a.get("total_files", 0),
42
+ "dirs": struct_b.get("total_dirs", 0) - struct_a.get("total_dirs", 0),
43
+ "modules": struct_b.get("modules", 0) - struct_a.get("modules", 0),
44
+ "max_depth": struct_b.get("max_depth", 0) - struct_a.get("max_depth", 0),
45
+ }
46
+
47
+ # 3. Language Deltas
48
+ langs_a = profile_a.get("languages", {}).get("languages", {})
49
+ langs_b = profile_b.get("languages", {}).get("languages", {})
50
+ all_langs = sorted(set(langs_a.keys()) | set(langs_b.keys()))
51
+
52
+ language_deltas = {}
53
+ for lang in all_langs:
54
+ pct_a = langs_a.get(lang, {}).get("percentage", 0.0)
55
+ pct_b = langs_b.get(lang, {}).get("percentage", 0.0)
56
+ language_deltas[lang] = {
57
+ "pct_a": pct_a,
58
+ "pct_b": pct_b,
59
+ "delta": round(pct_b - pct_a, 2),
60
+ }
61
+
62
+ languages_added = [lang for lang in all_langs if lang in langs_b and lang not in langs_a]
63
+ languages_removed = [lang for lang in all_langs if lang in langs_a and lang not in langs_b]
64
+
65
+ # 4. Architecture Shift
66
+ arch_a = profile_a.get("architecture", {})
67
+ arch_b = profile_b.get("architecture", {})
68
+ pattern_a = profile_a.get("system_type", arch_a.get("primary_pattern", "Unknown"))
69
+ pattern_b = profile_b.get("system_type", arch_b.get("primary_pattern", "Unknown"))
70
+ traits_a = set(arch_a.get("traits", []))
71
+ traits_b = set(arch_b.get("traits", []))
72
+
73
+ architecture_shift = {
74
+ "from_pattern": pattern_a,
75
+ "to_pattern": pattern_b,
76
+ "pattern_changed": pattern_a != pattern_b,
77
+ "traits_added": sorted(traits_b - traits_a),
78
+ "traits_removed": sorted(traits_a - traits_b),
79
+ "coupling_a": arch_a.get("coupling", "Unknown"),
80
+ "coupling_b": arch_b.get("coupling", "Unknown"),
81
+ }
82
+
83
+ # 5. Dependency Graph Shift
84
+ deps_a = profile_a.get("dependencies", {})
85
+ deps_b = profile_b.get("dependencies", {})
86
+ has_circ_a = deps_a.get("has_circular_deps", False)
87
+ has_circ_b = deps_b.get("has_circular_deps", False)
88
+
89
+ if not has_circ_a and has_circ_b:
90
+ circ_status = "introduced"
91
+ elif has_circ_a and not has_circ_b:
92
+ circ_status = "resolved"
93
+ elif has_circ_a and has_circ_b:
94
+ circ_status = "persisting"
95
+ else:
96
+ circ_status = "clean"
97
+
98
+ dependency_shift = {
99
+ "modules_delta": deps_b.get("total_modules", 0) - deps_a.get("total_modules", 0),
100
+ "edges_delta": deps_b.get("total_edges", 0) - deps_a.get("total_edges", 0),
101
+ "density_delta": round(deps_b.get("density", 0.0) - deps_a.get("density", 0.0), 4),
102
+ "circular_deps": {
103
+ "from": has_circ_a,
104
+ "to": has_circ_b,
105
+ "status": circ_status,
106
+ "cycles_delta": deps_b.get("circular_count", 0) - deps_a.get("circular_count", 0),
107
+ },
108
+ }
109
+
110
+ # 6. Health & Quality Shift
111
+ health_a = profile_a.get("health", {})
112
+ health_b = profile_b.get("health", {})
113
+ overall_a = health_a.get("overall", "Unknown")
114
+ overall_b = health_b.get("overall", "Unknown")
115
+ rank_a = self.HEALTH_RANKS.get(overall_a, 0)
116
+ rank_b = self.HEALTH_RANKS.get(overall_b, 0)
117
+
118
+ if rank_b > rank_a:
119
+ health_status = "improved"
120
+ elif rank_b < rank_a:
121
+ health_status = "degraded"
122
+ else:
123
+ health_status = "unchanged"
124
+
125
+ counts_a = health_a.get("severity_counts", {})
126
+ counts_b = health_b.get("severity_counts", {})
127
+ sec_a = profile_a.get("security", {})
128
+ sec_b = profile_b.get("security", {})
129
+
130
+ health_shift = {
131
+ "from_health": overall_a,
132
+ "to_health": overall_b,
133
+ "status": health_status,
134
+ "critical_smells_delta": counts_b.get("critical", 0) - counts_a.get("critical", 0),
135
+ "warning_smells_delta": counts_b.get("warning", 0) - counts_a.get("warning", 0),
136
+ "critical_vulns_delta": sec_b.get("total_critical", 0) - sec_a.get("total_critical", 0),
137
+ "secrets_introduced": (not sec_a.get("has_secrets", False)) and sec_b.get("has_secrets", False),
138
+ "secrets_resolved": sec_a.get("has_secrets", False) and (not sec_b.get("has_secrets", False)),
139
+ }
140
+
141
+ # 7. Developer Genome Shift
142
+ dev_a = profile_a.get("developer_genome", {})
143
+ dev_b = profile_b.get("developer_genome", {})
144
+ developer_shift = {
145
+ "contributors_delta": dev_b.get("total_contributors", 0) - dev_a.get("total_contributors", 0),
146
+ "bus_factor_delta": dev_b.get("bus_factor", 0) - dev_a.get("bus_factor", 0),
147
+ }
148
+
149
+ # 8. Mutation Verdict
150
+ verdict = self._determine_verdict(similarity_score, health_shift, architecture_shift, dependency_shift)
151
+
152
+ return {
153
+ "metadata": {
154
+ "source_a": source_a,
155
+ "source_b": source_b,
156
+ "compared_at": datetime.now().isoformat(),
157
+ },
158
+ "similarity_score": similarity_score,
159
+ "mutation_verdict": verdict,
160
+ "structure_delta": structure_delta,
161
+ "language_deltas": language_deltas,
162
+ "languages_added": languages_added,
163
+ "languages_removed": languages_removed,
164
+ "architecture_shift": architecture_shift,
165
+ "dependency_shift": dependency_shift,
166
+ "health_shift": health_shift,
167
+ "developer_shift": developer_shift,
168
+ }
169
+
170
+ def compute_similarity(self, profile_a: dict, profile_b: dict) -> float:
171
+ """Compute genetic similarity score between 0.0% and 100.0%."""
172
+ # 1. Language overlap (cosine similarity of percentage vectors) - weight: 25%
173
+ langs_a = profile_a.get("languages", {}).get("languages", {})
174
+ langs_b = profile_b.get("languages", {}).get("languages", {})
175
+ all_langs = set(langs_a.keys()) | set(langs_b.keys())
176
+
177
+ if not all_langs:
178
+ lang_sim = 1.0
179
+ else:
180
+ vec_a = [langs_a.get(lang, {}).get("percentage", 0.0) for lang in all_langs]
181
+ vec_b = [langs_b.get(lang, {}).get("percentage", 0.0) for lang in all_langs]
182
+ dot = sum(a * b for a, b in zip(vec_a, vec_b))
183
+ norm_a = math.sqrt(sum(a * a for a in vec_a))
184
+ norm_b = math.sqrt(sum(b * b for b in vec_b))
185
+ lang_sim = (dot / (norm_a * norm_b)) if (norm_a > 0 and norm_b > 0) else 0.0
186
+
187
+ # 2. Structure size similarity - weight: 20%
188
+ files_a = profile_a.get("structure_stats", {}).get("total_files", 0)
189
+ files_b = profile_b.get("structure_stats", {}).get("total_files", 0)
190
+ if files_a == 0 and files_b == 0:
191
+ struct_sim = 1.0
192
+ elif files_a == 0 or files_b == 0:
193
+ struct_sim = 0.0
194
+ else:
195
+ struct_sim = min(files_a, files_b) / max(files_a, files_b)
196
+
197
+ # 3. Architecture & Traits similarity - weight: 25%
198
+ arch_a = profile_a.get("architecture", {})
199
+ arch_b = profile_b.get("architecture", {})
200
+ pattern_a = profile_a.get("system_type", arch_a.get("primary_pattern", ""))
201
+ pattern_b = profile_b.get("system_type", arch_b.get("primary_pattern", ""))
202
+ pattern_match = 1.0 if pattern_a == pattern_b else 0.0
203
+
204
+ traits_a = set(arch_a.get("traits", []))
205
+ traits_b = set(arch_b.get("traits", []))
206
+ if not traits_a and not traits_b:
207
+ traits_sim = 1.0
208
+ else:
209
+ union = traits_a | traits_b
210
+ traits_sim = (len(traits_a & traits_b) / len(union)) if union else 1.0
211
+
212
+ arch_sim = (0.6 * pattern_match) + (0.4 * traits_sim)
213
+
214
+ # 4. Dependency topology similarity - weight: 15%
215
+ deps_a = profile_a.get("dependencies", {})
216
+ deps_b = profile_b.get("dependencies", {})
217
+ circ_match = 1.0 if deps_a.get("has_circular_deps") == deps_b.get("has_circular_deps") else 0.0
218
+ edges_a = deps_a.get("total_edges", 0)
219
+ edges_b = deps_b.get("total_edges", 0)
220
+ if edges_a == 0 and edges_b == 0:
221
+ edge_sim = 1.0
222
+ elif edges_a == 0 or edges_b == 0:
223
+ edge_sim = 0.2
224
+ else:
225
+ edge_sim = min(edges_a, edges_b) / max(edges_a, edges_b)
226
+ dep_sim = (0.5 * circ_match) + (0.5 * edge_sim)
227
+
228
+ # 5. Health & Quality Alignment - weight: 15%
229
+ health_a = profile_a.get("health", {}).get("overall", "Unknown")
230
+ health_b = profile_b.get("health", {}).get("overall", "Unknown")
231
+ rank_a = self.HEALTH_RANKS.get(health_a, 2)
232
+ rank_b = self.HEALTH_RANKS.get(health_b, 2)
233
+ health_diff = abs(rank_a - rank_b)
234
+ health_sim = max(0.0, 1.0 - (health_diff / 3.0))
235
+
236
+ # Composite score
237
+ composite = (
238
+ 0.25 * lang_sim
239
+ + 0.20 * struct_sim
240
+ + 0.25 * arch_sim
241
+ + 0.15 * dep_sim
242
+ + 0.15 * health_sim
243
+ )
244
+ return round(composite * 100.0, 1)
245
+
246
+ def _determine_verdict(
247
+ self,
248
+ similarity: float,
249
+ health_shift: dict,
250
+ arch_shift: dict,
251
+ dep_shift: dict,
252
+ ) -> str:
253
+ """Formulate a pragmatic developer verdict for the mutation."""
254
+ if similarity >= 99.5 and not health_shift["secrets_introduced"]:
255
+ return "Identical Genome"
256
+
257
+ if health_shift["secrets_introduced"]:
258
+ return "Critical Regression (Secrets Exposed)"
259
+
260
+ if health_shift["status"] == "degraded" or dep_shift["circular_deps"]["status"] == "introduced":
261
+ return "Architectural & Health Degradation"
262
+
263
+ if health_shift["status"] == "improved" or dep_shift["circular_deps"]["status"] == "resolved":
264
+ return "Quality & Hygiene Improvement"
265
+
266
+ if arch_shift["pattern_changed"]:
267
+ return "Paradigm Shift / Architectural Migration"
268
+
269
+ if similarity >= 85.0:
270
+ return "Stable Evolution / Conservative Refactor"
271
+
272
+ if similarity >= 60.0:
273
+ return "Moderate Architectural Mutation"
274
+
275
+ return "Radical Divergence / Genetic Drift"
276
+
277
+ def to_markdown(self, diff: dict) -> str:
278
+ """Format diff result as Markdown report."""
279
+ meta = diff["metadata"]
280
+ lines = [
281
+ "# 🧬 CodeDNA Genetic Drift & Mutation Diff\n",
282
+ f"> **Baseline (A):** `{meta['source_a']}`",
283
+ f"> **Target (B):** `{meta['source_b']}`",
284
+ f"> **Compared At:** {meta['compared_at'][:19]}",
285
+ f"> **Genetic Similarity:** **{diff['similarity_score']}%**",
286
+ f"> **Verdict:** `{diff['mutation_verdict']}`\n",
287
+ "---\n",
288
+ "## 📊 Structural & Architectural Comparison\n",
289
+ "| Metric | Baseline (A) | Target (B) | Delta / Shift |",
290
+ "| :--- | :--- | :--- | :--- |",
291
+ ]
292
+
293
+ # Files & Structure
294
+ s_delta = diff["structure_delta"]
295
+ lines.append(f"| **Files** | - | - | `{s_delta['files']:+d}` files |")
296
+ lines.append(f"| **Modules** | - | - | `{s_delta['modules']:+d}` modules |")
297
+
298
+ # Architecture Pattern
299
+ arch = diff["architecture_shift"]
300
+ arch_delta = "Unchanged" if not arch["pattern_changed"] else f"`{arch['from_pattern']}` ➔ `{arch['to_pattern']}`"
301
+ lines.append(f"| **Architecture** | `{arch['from_pattern']}` | `{arch['to_pattern']}` | {arch_delta} |")
302
+
303
+ # Health
304
+ h = diff["health_shift"]
305
+ h_delta = f"{h['status'].capitalize()} (`{h['critical_smells_delta']:+d}` crit, `{h['warning_smells_delta']:+d}` warn)"
306
+ lines.append(f"| **Health Rating** | `{h['from_health']}` | `{h['to_health']}` | {h_delta} |")
307
+
308
+ # Circular Deps
309
+ dep = diff["dependency_shift"]
310
+ c_status = dep["circular_deps"]["status"]
311
+ lines.append(f"| **Circular Deps** | `{'Yes' if dep['circular_deps']['from'] else 'No'}` | `{'Yes' if dep['circular_deps']['to'] else 'No'}` | `{c_status}` |")
312
+ lines.append(f"| **Dep Edges** | - | - | `{dep['edges_delta']:+d}` connections |")
313
+
314
+ # Developer Bus Factor
315
+ dev = diff["developer_shift"]
316
+ lines.append(f"| **Bus Factor** | - | - | `{dev['bus_factor_delta']:+d}` |")
317
+
318
+ lines.append("\n## 🌐 Language Composition Shifts\n")
319
+ lines.append("| Language | Baseline (A) % | Target (B) % | Shift |")
320
+ lines.append("| :--- | :--- | :--- | :--- |")
321
+ for lang, d in diff["language_deltas"].items():
322
+ if d["pct_a"] > 0 or d["pct_b"] > 0:
323
+ lines.append(f"| {lang} | {d['pct_a']}% | {d['pct_b']}% | `{d['delta']:+0.1f}%` |")
324
+
325
+ if diff["languages_added"]:
326
+ lines.append(f"\n- **Languages Added:** {', '.join(diff['languages_added'])}")
327
+ if diff["languages_removed"]:
328
+ lines.append(f"- **Languages Removed:** {', '.join(diff['languages_removed'])}")
329
+
330
+ lines.append("\n---\n*Generated by [CodeDNA Diff Engine](https://github.com/shenald-dev/codedna)*")
331
+ return "\n".join(lines)
332
+
333
+ @staticmethod
334
+ def evaluate_drift_gate(
335
+ diff: dict,
336
+ max_drift: float = 20.0,
337
+ min_similarity: float | None = None,
338
+ fail_on_circular: bool = True,
339
+ fail_on_health_drop: bool = True,
340
+ fail_on_secrets: bool = True,
341
+ allow_degradation: bool | None = None,
342
+ allow_circular_deps: bool | None = None,
343
+ allow_new_secrets: bool | None = None,
344
+ ) -> tuple[bool, list[str]]:
345
+ """Evaluate diff result against architectural drift regression policies.
346
+
347
+ Supports both policy-check naming conventions (fail_on_* and allow_*).
348
+
349
+ Returns:
350
+ Tuple of (passed: bool, failure_reasons: list[str])
351
+ """
352
+ if allow_degradation is not None:
353
+ fail_on_health_drop = not allow_degradation
354
+ if allow_circular_deps is not None:
355
+ fail_on_circular = not allow_circular_deps
356
+ if allow_new_secrets is not None:
357
+ fail_on_secrets = not allow_new_secrets
358
+
359
+ reasons = []
360
+ similarity = diff.get("similarity_score", 100.0)
361
+
362
+ if min_similarity is not None and min_similarity > 0.0:
363
+ min_allowed_similarity = min_similarity
364
+ else:
365
+ min_allowed_similarity = max(0.0, 100.0 - max_drift)
366
+
367
+ if similarity < min_allowed_similarity:
368
+ reasons.append(
369
+ f"Genetic similarity {similarity}% is below minimum required {min_allowed_similarity}%"
370
+ )
371
+
372
+ dep_shift = diff.get("dependency_shift", {})
373
+ circ_status = dep_shift.get("circular_deps", {}).get("status", "")
374
+ if fail_on_circular and circ_status == "introduced":
375
+ reasons.append("Circular dependencies were introduced in target codebase")
376
+
377
+ health_shift = diff.get("health_shift", {})
378
+ if fail_on_health_drop and health_shift.get("status") == "degraded":
379
+ reasons.append(
380
+ f"Overall health degraded from '{health_shift.get('from_health')}' to '{health_shift.get('to_health')}'"
381
+ )
382
+
383
+ if fail_on_secrets and health_shift.get("secrets_introduced"):
384
+ reasons.append("Hardcoded credentials or API keys were introduced in target codebase")
385
+
386
+ crit_delta = health_shift.get("critical_smells_delta", 0)
387
+ if crit_delta > 0:
388
+ reasons.append(f"Critical code smells increased by +{crit_delta}")
389
+
390
+ passed = len(reasons) == 0
391
+ return passed, reasons
392
+
393
+ def to_json(self, diff: dict) -> str:
394
+ """Format diff result as formatted JSON string."""
395
+ return json.dumps(diff, indent=2, default=str)