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.
- codedna/__init__.py +3 -0
- codedna/analyzers/__init__.py +1 -0
- codedna/analyzers/ai_analyzer.py +111 -0
- codedna/analyzers/architecture_detector.py +150 -0
- codedna/analyzers/cache_manager.py +70 -0
- codedna/analyzers/code_smell_detector.py +302 -0
- codedna/analyzers/complexity_analyzer.py +329 -0
- codedna/analyzers/dependency_mapper.py +171 -0
- codedna/analyzers/developer_analyzer.py +148 -0
- codedna/analyzers/diff_engine.py +395 -0
- codedna/analyzers/dna_generator.py +308 -0
- codedna/analyzers/evolution_engine.py +193 -0
- codedna/analyzers/github_analyzer.py +86 -0
- codedna/analyzers/language_detector.py +151 -0
- codedna/analyzers/repo_cloner.py +91 -0
- codedna/analyzers/security_detector.py +271 -0
- codedna/analyzers/structure_analyzer.py +91 -0
- codedna/ci/__init__.py +5 -0
- codedna/ci/gatekeeper.py +421 -0
- codedna/cli.py +839 -0
- codedna/compliance/__init__.py +5 -0
- codedna/compliance/sbom.py +410 -0
- codedna/config.py +167 -0
- codedna/mcp/__init__.py +5 -0
- codedna/mcp/protocol.py +65 -0
- codedna/mcp/server.py +393 -0
- codedna/remediation/__init__.py +5 -0
- codedna/remediation/planner.py +244 -0
- codedna/visualization/__init__.py +1 -0
- codedna/visualization/html_export.py +507 -0
- codedna/visualization/renderer.py +373 -0
- codedna_analyzer-1.4.0.dist-info/METADATA +425 -0
- codedna_analyzer-1.4.0.dist-info/RECORD +37 -0
- codedna_analyzer-1.4.0.dist-info/WHEEL +5 -0
- codedna_analyzer-1.4.0.dist-info/entry_points.txt +2 -0
- codedna_analyzer-1.4.0.dist-info/licenses/LICENSE +21 -0
- 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)
|