mcp-souschef 2.8.0__py3-none-any.whl → 3.0.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.
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.0.0.dist-info}/METADATA +82 -10
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.0.0.dist-info}/RECORD +23 -19
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.0.0.dist-info}/WHEEL +1 -1
- souschef/__init__.py +37 -5
- souschef/assessment.py +1248 -57
- souschef/ci/common.py +126 -0
- souschef/ci/github_actions.py +3 -92
- souschef/ci/gitlab_ci.py +2 -52
- souschef/ci/jenkins_pipeline.py +2 -59
- souschef/cli.py +117 -8
- souschef/converters/playbook.py +259 -90
- souschef/converters/resource.py +12 -11
- souschef/converters/template.py +177 -0
- souschef/core/metrics.py +313 -0
- souschef/core/validation.py +53 -0
- souschef/deployment.py +61 -9
- souschef/server.py +680 -0
- souschef/ui/app.py +469 -351
- souschef/ui/pages/ai_settings.py +74 -8
- souschef/ui/pages/cookbook_analysis.py +2467 -298
- souschef/ui/pages/validation_reports.py +274 -0
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.0.0.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-2.8.0.dist-info → mcp_souschef-3.0.0.dist-info}/licenses/LICENSE +0 -0
souschef/core/metrics.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Centralized metrics and effort calculation module for consistent time estimations."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import NamedTuple
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ComplexityLevel",
|
|
9
|
+
"EffortMetrics",
|
|
10
|
+
"convert_days_to_hours",
|
|
11
|
+
"convert_days_to_weeks",
|
|
12
|
+
"convert_hours_to_days",
|
|
13
|
+
"estimate_effort_for_complexity",
|
|
14
|
+
"get_team_recommendation",
|
|
15
|
+
"get_timeline_weeks",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ComplexityLevel(str, Enum):
|
|
20
|
+
"""Standard complexity levels used across all components."""
|
|
21
|
+
|
|
22
|
+
LOW = "low"
|
|
23
|
+
MEDIUM = "medium"
|
|
24
|
+
HIGH = "high"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class EffortMetrics:
|
|
29
|
+
"""
|
|
30
|
+
Centralized container for all effort estimates.
|
|
31
|
+
|
|
32
|
+
Provides consistent representations across different formats:
|
|
33
|
+
- Base unit: person-days (with decimal precision)
|
|
34
|
+
- Derived: hours, weeks with consistent conversion factors
|
|
35
|
+
- Ranges: For display purposes, converting days to week ranges
|
|
36
|
+
|
|
37
|
+
Ensures all components (migration planning, dependency mapping,
|
|
38
|
+
validation reports) use the same underlying numbers.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
estimated_days: float
|
|
42
|
+
"""Base unit: person-days (e.g., 2.5, 5.0, 10.0)"""
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def estimated_hours(self) -> float:
|
|
46
|
+
"""Convert days to hours using standard 8-hour workday."""
|
|
47
|
+
return self.estimated_days * 8
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def estimated_weeks_low(self) -> int:
|
|
51
|
+
"""Conservative estimate: assumes optimal parallelization."""
|
|
52
|
+
return max(1, int(self.estimated_days / 7))
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def estimated_weeks_high(self) -> int:
|
|
56
|
+
"""Realistic estimate: assumes sequential/limited parallelization."""
|
|
57
|
+
return max(1, int(self.estimated_days / 3.5))
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def estimated_weeks_range(self) -> str:
|
|
61
|
+
"""Human-readable week range (e.g., '2-4 weeks')."""
|
|
62
|
+
low = self.estimated_weeks_low
|
|
63
|
+
high = self.estimated_weeks_high
|
|
64
|
+
if low == high:
|
|
65
|
+
return f"{low} week{'s' if low != 1 else ''}"
|
|
66
|
+
return f"{low}-{high} weeks"
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def estimated_days_formatted(self) -> str:
|
|
70
|
+
"""Formatted days with appropriate precision."""
|
|
71
|
+
if self.estimated_days == int(self.estimated_days):
|
|
72
|
+
return f"{int(self.estimated_days)} days"
|
|
73
|
+
return f"{self.estimated_days:.1f} days"
|
|
74
|
+
|
|
75
|
+
def __str__(self) -> str:
|
|
76
|
+
"""Return a string representation of effort metrics."""
|
|
77
|
+
return f"{self.estimated_days_formatted} ({self.estimated_weeks_range})"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TeamRecommendation(NamedTuple):
|
|
81
|
+
"""Team composition and timeline recommendation."""
|
|
82
|
+
|
|
83
|
+
team_size: str
|
|
84
|
+
"""e.g., '1 developer + 1 reviewer'"""
|
|
85
|
+
|
|
86
|
+
timeline_weeks: int
|
|
87
|
+
"""Recommended timeline in weeks"""
|
|
88
|
+
|
|
89
|
+
description: str
|
|
90
|
+
"""Human-readable description of the recommendation"""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Conversion constants - Single source of truth
|
|
94
|
+
HOURS_PER_WORKDAY = 8
|
|
95
|
+
DAYS_PER_WEEK = 7
|
|
96
|
+
|
|
97
|
+
# Complexity thresholds for automatic categorization
|
|
98
|
+
COMPLEXITY_THRESHOLD_LOW = 30
|
|
99
|
+
COMPLEXITY_THRESHOLD_HIGH = 70
|
|
100
|
+
|
|
101
|
+
# Effort multiplier per resource (base 1 resource = baseline effort)
|
|
102
|
+
EFFORT_MULTIPLIER_PER_RESOURCE = 0.125 # 0.125 days = 1 hour per resource
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def convert_days_to_hours(days: float) -> float:
|
|
106
|
+
"""Convert person-days to hours using standard 8-hour workday."""
|
|
107
|
+
return days * HOURS_PER_WORKDAY
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def convert_hours_to_days(hours: float) -> float:
|
|
111
|
+
"""Convert hours to person-days using standard 8-hour workday."""
|
|
112
|
+
return hours / HOURS_PER_WORKDAY
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def convert_days_to_weeks(days: float, conservative: bool = False) -> int:
|
|
116
|
+
"""
|
|
117
|
+
Convert days to weeks estimate.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
days: Number of person-days
|
|
121
|
+
conservative: If True, use realistic estimate (1 engineer, limited
|
|
122
|
+
parallelization). If False, use optimistic estimate (full
|
|
123
|
+
parallelization)
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Number of weeks (integer)
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
weeks = days / 3.5 if conservative else days / DAYS_PER_WEEK
|
|
130
|
+
return max(1, int(weeks))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def estimate_effort_for_complexity(
|
|
134
|
+
complexity_score: float, resource_count: int = 1
|
|
135
|
+
) -> EffortMetrics:
|
|
136
|
+
"""
|
|
137
|
+
Estimate effort based on complexity score and resource count.
|
|
138
|
+
|
|
139
|
+
Provides consistent effort estimation across all components.
|
|
140
|
+
|
|
141
|
+
Formula:
|
|
142
|
+
- Base effort: resource_count * 0.125 days per recipe/resource
|
|
143
|
+
- Complexity multiplier: 1.0 + (complexity_score / 100)
|
|
144
|
+
- Final effort: base_effort * complexity_multiplier
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
complexity_score: Score from 0-100 (0=simple, 100=complex)
|
|
148
|
+
resource_count: Number of resources to migrate (recipes, templates, etc.)
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
EffortMetrics object with all representations
|
|
152
|
+
|
|
153
|
+
"""
|
|
154
|
+
base_effort = resource_count * EFFORT_MULTIPLIER_PER_RESOURCE
|
|
155
|
+
complexity_multiplier = 1.0 + (complexity_score / 100)
|
|
156
|
+
estimated_days = base_effort * complexity_multiplier
|
|
157
|
+
|
|
158
|
+
return EffortMetrics(estimated_days=round(estimated_days, 1))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def categorize_complexity(score: float) -> ComplexityLevel:
|
|
162
|
+
"""
|
|
163
|
+
Categorize complexity score into standard levels.
|
|
164
|
+
|
|
165
|
+
Consistent thresholds across all components:
|
|
166
|
+
- Low: 0-29
|
|
167
|
+
- Medium: 30-69
|
|
168
|
+
- High: 70-100
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
score: Complexity score from 0-100
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
ComplexityLevel enum value
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
if score < COMPLEXITY_THRESHOLD_LOW:
|
|
178
|
+
return ComplexityLevel.LOW
|
|
179
|
+
elif score < COMPLEXITY_THRESHOLD_HIGH:
|
|
180
|
+
return ComplexityLevel.MEDIUM
|
|
181
|
+
else:
|
|
182
|
+
return ComplexityLevel.HIGH
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_team_recommendation(total_effort_days: float) -> TeamRecommendation:
|
|
186
|
+
"""
|
|
187
|
+
Get team composition and timeline recommendation based on total effort.
|
|
188
|
+
|
|
189
|
+
Consistent recommendations across all components.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
total_effort_days: Total person-days of effort
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
TeamRecommendation with team size and timeline
|
|
196
|
+
|
|
197
|
+
"""
|
|
198
|
+
if total_effort_days < 20:
|
|
199
|
+
return TeamRecommendation(
|
|
200
|
+
team_size="1 developer + 1 reviewer",
|
|
201
|
+
timeline_weeks=4,
|
|
202
|
+
description="Single developer with oversight",
|
|
203
|
+
)
|
|
204
|
+
elif total_effort_days < 50:
|
|
205
|
+
return TeamRecommendation(
|
|
206
|
+
team_size="2 developers + 1 senior reviewer",
|
|
207
|
+
timeline_weeks=6,
|
|
208
|
+
description="Small dedicated team",
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
return TeamRecommendation(
|
|
212
|
+
team_size="3-4 developers + 1 tech lead + 1 architect",
|
|
213
|
+
timeline_weeks=10,
|
|
214
|
+
description="Large dedicated migration team",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_timeline_weeks(total_effort_days: float, strategy: str = "phased") -> int:
|
|
219
|
+
"""
|
|
220
|
+
Calculate recommended timeline in weeks based on effort and strategy.
|
|
221
|
+
|
|
222
|
+
Consistent timeline calculation across planning, dependency mapping, and reports.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
total_effort_days: Total person-days estimated
|
|
226
|
+
strategy: Migration strategy ('phased', 'big_bang', 'parallel')
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Recommended timeline in weeks
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
# Base calculation: distribute effort across team capacity
|
|
233
|
+
# Assume 3-5 person-days of output per week with normal team capacity
|
|
234
|
+
base_weeks = max(2, int(total_effort_days / 4.5))
|
|
235
|
+
|
|
236
|
+
# Apply strategy adjustments
|
|
237
|
+
if strategy == "phased":
|
|
238
|
+
# Phased adds overhead for testing between phases
|
|
239
|
+
return int(base_weeks * 1.1)
|
|
240
|
+
elif strategy == "big_bang":
|
|
241
|
+
# Big bang is faster but riskier
|
|
242
|
+
return int(base_weeks * 0.9)
|
|
243
|
+
else: # parallel
|
|
244
|
+
# Parallel has some overhead for coordination
|
|
245
|
+
return int(base_weeks * 1.05)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def validate_metrics_consistency(
|
|
249
|
+
days: float, weeks: str, hours: float, complexity: str
|
|
250
|
+
) -> tuple[bool, list[str]]:
|
|
251
|
+
"""
|
|
252
|
+
Validate that different metric representations are consistent.
|
|
253
|
+
|
|
254
|
+
Used for validation reports to catch contradictions.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
days: Days estimate
|
|
258
|
+
weeks: Weeks range string (e.g., "2-4 weeks")
|
|
259
|
+
hours: Hours estimate
|
|
260
|
+
complexity: Complexity level string
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Tuple of (is_valid, list_of_errors)
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
errors = []
|
|
267
|
+
|
|
268
|
+
# Check hours consistency
|
|
269
|
+
expected_hours = days * 8
|
|
270
|
+
if abs(hours - expected_hours) > 1.0: # Allow 1 hour tolerance
|
|
271
|
+
errors.append(
|
|
272
|
+
f"Hours mismatch: {hours:.1f} hours but {days} days = "
|
|
273
|
+
f"{expected_hours:.1f} hours"
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Check weeks consistency (loose check due to range)
|
|
277
|
+
# Valid formats: "1 week", "1-2 weeks"
|
|
278
|
+
if "week" not in weeks.lower():
|
|
279
|
+
errors.append(f"Invalid weeks format: {weeks}")
|
|
280
|
+
elif "-" in weeks:
|
|
281
|
+
# Range format: "X-Y weeks"
|
|
282
|
+
try:
|
|
283
|
+
parts = weeks.replace(" weeks", "").replace(" week", "").split("-")
|
|
284
|
+
week_min = int(parts[0].strip())
|
|
285
|
+
week_max = int(parts[1].strip())
|
|
286
|
+
expected_weeks = int(days / 3.5) # Conservative estimate
|
|
287
|
+
|
|
288
|
+
if not (week_min <= expected_weeks <= week_max + 1):
|
|
289
|
+
errors.append(
|
|
290
|
+
f"Weeks mismatch: {weeks} but {days} days should be "
|
|
291
|
+
f"approximately {expected_weeks} weeks"
|
|
292
|
+
)
|
|
293
|
+
except (ValueError, IndexError):
|
|
294
|
+
errors.append(f"Invalid weeks format: {weeks}")
|
|
295
|
+
else:
|
|
296
|
+
# Single week format: "X week" or "X weeks"
|
|
297
|
+
try:
|
|
298
|
+
num = int(weeks.replace(" weeks", "").replace(" week", "").strip())
|
|
299
|
+
expected_weeks = int(days / 3.5)
|
|
300
|
+
if num != expected_weeks and abs(num - expected_weeks) > 2:
|
|
301
|
+
errors.append(
|
|
302
|
+
f"Weeks mismatch: {weeks} but {days} days should be "
|
|
303
|
+
f"approximately {expected_weeks} weeks"
|
|
304
|
+
)
|
|
305
|
+
except ValueError:
|
|
306
|
+
errors.append(f"Invalid weeks format: {weeks}")
|
|
307
|
+
|
|
308
|
+
# Check complexity is valid
|
|
309
|
+
valid_complexities = {level.value for level in ComplexityLevel}
|
|
310
|
+
if complexity.lower() not in valid_complexities:
|
|
311
|
+
errors.append(f"Invalid complexity level: {complexity}")
|
|
312
|
+
|
|
313
|
+
return len(errors) == 0, errors
|
souschef/core/validation.py
CHANGED
|
@@ -586,3 +586,56 @@ class ValidationEngine:
|
|
|
586
586
|
elif result.level == ValidationLevel.INFO:
|
|
587
587
|
summary["info"] += 1
|
|
588
588
|
return summary
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _format_validation_results_summary(
|
|
592
|
+
conversion_type: str, summary: dict[str, int]
|
|
593
|
+
) -> str:
|
|
594
|
+
"""
|
|
595
|
+
Format validation results as a summary.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
conversion_type: Type of conversion.
|
|
599
|
+
summary: Summary of validation results.
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
Formatted summary output.
|
|
603
|
+
|
|
604
|
+
"""
|
|
605
|
+
total_issues = summary["errors"] + summary["warnings"] + summary["info"]
|
|
606
|
+
|
|
607
|
+
if total_issues == 0:
|
|
608
|
+
return f"""# Validation Summary for {conversion_type} Conversion
|
|
609
|
+
|
|
610
|
+
✅ **All validation checks passed!** No issues found.
|
|
611
|
+
|
|
612
|
+
Errors: 0
|
|
613
|
+
Warnings: 0
|
|
614
|
+
Info: 0
|
|
615
|
+
"""
|
|
616
|
+
|
|
617
|
+
# Determine status icon based on error/warning counts
|
|
618
|
+
if summary["errors"] > 0:
|
|
619
|
+
status_icon = "❌"
|
|
620
|
+
elif summary["warnings"] > 0:
|
|
621
|
+
status_icon = "⚠️"
|
|
622
|
+
else:
|
|
623
|
+
status_icon = "ℹ️"
|
|
624
|
+
|
|
625
|
+
# Determine status message based on error/warning counts
|
|
626
|
+
if summary["errors"] > 0:
|
|
627
|
+
status = "Failed"
|
|
628
|
+
elif summary["warnings"] > 0:
|
|
629
|
+
status = "Warning"
|
|
630
|
+
else:
|
|
631
|
+
status = "Passed with info"
|
|
632
|
+
|
|
633
|
+
return f"""# Validation Summary for {conversion_type} Conversion
|
|
634
|
+
|
|
635
|
+
{status_icon} **Validation Results:**
|
|
636
|
+
• Errors: {summary["errors"]}
|
|
637
|
+
• Warnings: {summary["warnings"]}
|
|
638
|
+
• Info: {summary["info"]}
|
|
639
|
+
|
|
640
|
+
**Status:** {status}
|
|
641
|
+
"""
|
souschef/deployment.py
CHANGED
|
@@ -21,6 +21,11 @@ from souschef.core.errors import (
|
|
|
21
21
|
validate_cookbook_structure,
|
|
22
22
|
validate_directory_exists,
|
|
23
23
|
)
|
|
24
|
+
from souschef.core.metrics import (
|
|
25
|
+
ComplexityLevel,
|
|
26
|
+
EffortMetrics,
|
|
27
|
+
categorize_complexity,
|
|
28
|
+
)
|
|
24
29
|
from souschef.core.path_utils import _safe_join
|
|
25
30
|
|
|
26
31
|
# Maximum length for attribute values in Chef attribute parsing
|
|
@@ -1496,13 +1501,56 @@ def _detect_patterns_from_content(content: str) -> list[str]:
|
|
|
1496
1501
|
return patterns
|
|
1497
1502
|
|
|
1498
1503
|
|
|
1499
|
-
def _assess_complexity_from_resource_count(
|
|
1500
|
-
|
|
1504
|
+
def _assess_complexity_from_resource_count(
|
|
1505
|
+
resource_count: int,
|
|
1506
|
+
) -> tuple[ComplexityLevel, str, str]:
|
|
1507
|
+
"""
|
|
1508
|
+
Assess complexity, effort estimate, and risk based on resource count.
|
|
1509
|
+
|
|
1510
|
+
Uses centralized metrics for consistent complexity categorization.
|
|
1511
|
+
|
|
1512
|
+
Args:
|
|
1513
|
+
resource_count: Number of resources in cookbook
|
|
1514
|
+
|
|
1515
|
+
Returns:
|
|
1516
|
+
Tuple of (complexity_level, effort_estimate_weeks, risk_level)
|
|
1517
|
+
|
|
1518
|
+
"""
|
|
1519
|
+
# Map resource count to complexity score (0-100 scale)
|
|
1520
|
+
# 50+ resources = high complexity (70-100)
|
|
1521
|
+
# 20-50 resources = medium complexity (30-69)
|
|
1522
|
+
# <20 resources = low complexity (0-29)
|
|
1501
1523
|
if resource_count > 50:
|
|
1502
|
-
|
|
1503
|
-
elif resource_count
|
|
1504
|
-
|
|
1505
|
-
|
|
1524
|
+
complexity_score = 80
|
|
1525
|
+
elif resource_count > 30:
|
|
1526
|
+
complexity_score = 50
|
|
1527
|
+
elif resource_count >= 20:
|
|
1528
|
+
complexity_score = 40
|
|
1529
|
+
else:
|
|
1530
|
+
complexity_score = 15
|
|
1531
|
+
|
|
1532
|
+
# Use centralized categorization
|
|
1533
|
+
complexity_level = categorize_complexity(complexity_score)
|
|
1534
|
+
|
|
1535
|
+
# Estimate effort based on resource count and complexity
|
|
1536
|
+
# Base: 0.2 days per resource (2.5 hours)
|
|
1537
|
+
base_days = resource_count * 0.2
|
|
1538
|
+
complexity_multiplier = 1 + (complexity_score / 100)
|
|
1539
|
+
estimated_days = round(base_days * complexity_multiplier, 1)
|
|
1540
|
+
|
|
1541
|
+
# Create metrics object for consistent week calculation
|
|
1542
|
+
metrics = EffortMetrics(estimated_days=estimated_days)
|
|
1543
|
+
effort_estimate = metrics.estimated_weeks_range
|
|
1544
|
+
|
|
1545
|
+
# Risk mapping based on complexity level
|
|
1546
|
+
if complexity_level == ComplexityLevel.HIGH:
|
|
1547
|
+
risk_level = "high"
|
|
1548
|
+
elif complexity_level == ComplexityLevel.MEDIUM:
|
|
1549
|
+
risk_level = "medium"
|
|
1550
|
+
else:
|
|
1551
|
+
risk_level = "low"
|
|
1552
|
+
|
|
1553
|
+
return complexity_level, effort_estimate, risk_level
|
|
1506
1554
|
|
|
1507
1555
|
|
|
1508
1556
|
def _analyse_application_cookbook(cookbook_path: Path, app_type: str) -> dict:
|
|
@@ -1536,10 +1584,14 @@ def _analyse_application_cookbook(cookbook_path: Path, app_type: str) -> dict:
|
|
|
1536
1584
|
# Silently skip malformed files
|
|
1537
1585
|
pass
|
|
1538
1586
|
|
|
1539
|
-
# Assess complexity
|
|
1587
|
+
# Assess complexity using centralized function
|
|
1540
1588
|
resource_count = len(analysis["resources"])
|
|
1541
|
-
|
|
1542
|
-
|
|
1589
|
+
complexity_level, effort, risk = _assess_complexity_from_resource_count(
|
|
1590
|
+
resource_count
|
|
1591
|
+
)
|
|
1592
|
+
|
|
1593
|
+
# Convert complexity level enum to string for backward compatibility
|
|
1594
|
+
analysis["complexity"] = complexity_level.value
|
|
1543
1595
|
analysis["effort_estimate"] = effort
|
|
1544
1596
|
analysis["risk_level"] = risk
|
|
1545
1597
|
|