codedd-cli 0.1.8__tar.gz → 0.1.9__tar.gz
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.
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/PKG-INFO +2 -2
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/__init__.py +1 -1
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/archetype_classifier.py +232 -6
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/architecture_analyzer.py +368 -73
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/MANIFEST.json +8 -4
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/enhanced_technology_detector.py +81 -3
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/repo_kind.py +5 -0
- codedd_cli-0.1.9/codedd_cli/auditor/server_parity/taxonomy.py +500 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/cli.py +3 -2
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/audit_cmd.py +5 -3
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/scope_cmd.py +86 -6
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/pyproject.toml +3 -2
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/LICENSE +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/README.md +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/__main__.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/api/__init__.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/api/client.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/api/endpoints.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/api/exceptions.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/__init__.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/architecture_prompts.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/architecture_schema_contract.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/audit_checkpoint.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/audit_progress_sync.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/commit_classifier.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/complexity_analyzer.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/cross_repo_evidence.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/dependency_scanner.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/file_auditor.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/git_stats_collector.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/parity_overrides.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/response_parser.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/__init__.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/enhanced_relationship_detector.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/evidence_extractor.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/infra_scan.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/manifest_coordinate_resolver.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/published_identity.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/repo_file.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/tech_normalizer.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/technology_lists.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/tier_definitions.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/sub_audit_runner.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/tier_definitions.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/__init__.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/agents.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/cli_ai_auditor.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/cli_orchestrator.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/cli_settings.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/file_io.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/__init__.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/_generic.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/c_cpp.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/csharp.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/go.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/java.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/js_ts.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/kotlin.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/php.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/ruby.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/rust.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/scala.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/shell.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/swift.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/taint.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_snippets.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/langgraph_runner.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/planner.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/prompts.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/scope.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/security_patterns.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/semgrep_runner.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/state.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/tools.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_validator.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auth/__init__.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auth/session.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auth/token_manager.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/__init__.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/ai_docs_cmd.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/audits_cmd.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/auth_cmd.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/config_cmd.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/fix_cmd.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/config/__init__.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/config/constants.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/config/fix_session.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/config/settings.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/config/url_validation.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/llm/__init__.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/llm/anthropic_utils.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/llm/key_manager.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/models/__init__.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/models/account.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/models/audit.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/models/local_directory.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/scanner/__init__.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/scanner/file_classifier.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/scanner/file_walker.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/scanner/line_counter.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/utils/__init__.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/utils/directory_validator.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/utils/display.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/utils/payload_inspector.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/utils/safe_path.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/utils/security.py +0 -0
- {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/utils/validators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codedd-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: CLI tool for CodeDD — run code audits from your terminal
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -32,7 +32,7 @@ Requires-Dist: radon (>=6.0.0)
|
|
|
32
32
|
Requires-Dist: rich (>=13.0.0)
|
|
33
33
|
Requires-Dist: tomli (>=2.0.0) ; python_version < "3.11"
|
|
34
34
|
Requires-Dist: tomli-w (>=1.0.0)
|
|
35
|
-
Requires-Dist: typer
|
|
35
|
+
Requires-Dist: typer (>=0.9.0)
|
|
36
36
|
Project-URL: Homepage, https://codedd.ai
|
|
37
37
|
Project-URL: Repository, https://gitlab.com/codedd1/codedd-cli
|
|
38
38
|
Description-Content-Type: text/markdown
|
|
@@ -35,11 +35,63 @@ except ImportError: # pragma: no cover - pyyaml is a hard dep, but stay safe
|
|
|
35
35
|
# from the streamed audit plan are observed by every accessor below.
|
|
36
36
|
from codedd_cli.auditor.server_parity import repo_kind as rk
|
|
37
37
|
from codedd_cli.auditor.server_parity import tier_definitions as td
|
|
38
|
+
from codedd_cli.auditor.server_parity.taxonomy import normalize_category, normalize_edge_type
|
|
38
39
|
from codedd_cli.auditor.server_parity.tech_normalizer import canonical_tech_name
|
|
39
40
|
|
|
40
41
|
logger = logging.getLogger(__name__)
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# Architecture style (topology) - orthogonal to the operational-scale tier
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
ARCHITECTURE_STYLE_LABELS: dict[str, str] = {
|
|
49
|
+
'microservices': 'Microservices',
|
|
50
|
+
'service_oriented': 'Service-Oriented (SOA)',
|
|
51
|
+
'modular_monolith': 'Modular Monolith',
|
|
52
|
+
'monolith': 'Monolith',
|
|
53
|
+
'library': 'Library / CLI Tool',
|
|
54
|
+
'data_pipeline': 'Data Pipeline',
|
|
55
|
+
'mobile_client': 'Mobile Client',
|
|
56
|
+
'serverless': 'Serverless',
|
|
57
|
+
'infrastructure': 'Infrastructure / IaC',
|
|
58
|
+
'documentation': 'Documentation',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def derive_architecture_style(
|
|
63
|
+
deployable_units: int,
|
|
64
|
+
logical_units: int,
|
|
65
|
+
service_node_count: int,
|
|
66
|
+
inter_service_edge_count: int,
|
|
67
|
+
detected_patterns: list[str],
|
|
68
|
+
repo_kind: str = 'service',
|
|
69
|
+
module_dirs: int = 0,
|
|
70
|
+
) -> str:
|
|
71
|
+
"""Topology-derived architecture style, independent of the tier ladder."""
|
|
72
|
+
kind = str(repo_kind or 'service').strip().lower()
|
|
73
|
+
if kind in rk.ABSTAIN_REPO_KINDS:
|
|
74
|
+
return kind
|
|
75
|
+
|
|
76
|
+
patterns_l = [str(p).lower() for p in (detected_patterns or [])]
|
|
77
|
+
graph_services = service_node_count if inter_service_edge_count >= 2 else 0
|
|
78
|
+
effective_services = max(int(deployable_units or 0), graph_services)
|
|
79
|
+
|
|
80
|
+
if effective_services >= 8 and (
|
|
81
|
+
inter_service_edge_count >= 4 or deployable_units >= 8
|
|
82
|
+
):
|
|
83
|
+
return 'microservices'
|
|
84
|
+
if effective_services >= 4:
|
|
85
|
+
return 'service_oriented'
|
|
86
|
+
if (
|
|
87
|
+
module_dirs >= 3
|
|
88
|
+
or logical_units >= 4
|
|
89
|
+
or any('modular monolith' in p or 'bounded context' in p for p in patterns_l)
|
|
90
|
+
):
|
|
91
|
+
return 'modular_monolith'
|
|
92
|
+
return 'monolith'
|
|
93
|
+
|
|
94
|
+
|
|
43
95
|
# ---------------------------------------------------------------------------
|
|
44
96
|
# Dimension Scorers
|
|
45
97
|
# ---------------------------------------------------------------------------
|
|
@@ -84,8 +136,114 @@ class DimensionScorer:
|
|
|
84
136
|
self.nodes = self.phase3.get('nodes', [])
|
|
85
137
|
self.edges = self.phase3.get('edges', [])
|
|
86
138
|
|
|
139
|
+
self.repo_files = self._resolve_repo_file_inventory()
|
|
140
|
+
|
|
87
141
|
self._tech_credit_ledger: dict[str, set[str]] = {}
|
|
88
142
|
self.penalty_signals: dict[str, bool] = {}
|
|
143
|
+
self._deployable_census: tuple[int, int] | None = None
|
|
144
|
+
|
|
145
|
+
def _resolve_repo_file_inventory(self) -> list[str]:
|
|
146
|
+
"""Prefer the full inventory over the budget-capped LLM selection."""
|
|
147
|
+
full = self.phase1.get('all_repository_files')
|
|
148
|
+
if isinstance(full, list) and full:
|
|
149
|
+
return [str(f) for f in full if isinstance(f, str)]
|
|
150
|
+
fallback = (
|
|
151
|
+
self.phase1.get('relevant_files', [])
|
|
152
|
+
or self.phase2.get('phase1_files', [])
|
|
153
|
+
or []
|
|
154
|
+
)
|
|
155
|
+
return [str(f) for f in fallback if isinstance(f, str)]
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _is_first_party_service_node(node: dict) -> bool:
|
|
159
|
+
if not isinstance(node, dict):
|
|
160
|
+
return False
|
|
161
|
+
if node.get('is_external') or (node.get('properties') or {}).get('is_external'):
|
|
162
|
+
return False
|
|
163
|
+
if normalize_category(node.get('category')) == 'service':
|
|
164
|
+
return True
|
|
165
|
+
return node.get('type') in ('service', 'component', 'module')
|
|
166
|
+
|
|
167
|
+
_SYNC_RPC_EDGE_TYPES = frozenset({'api_calls', 'calls'})
|
|
168
|
+
|
|
169
|
+
def _count_inter_service_edges(
|
|
170
|
+
self, edge_types: frozenset | None = None
|
|
171
|
+
) -> tuple[int, int]:
|
|
172
|
+
"""Count graph edges connecting two distinct first-party service nodes."""
|
|
173
|
+
service_ids: set[str] = set()
|
|
174
|
+
for n in self.nodes:
|
|
175
|
+
if self._is_first_party_service_node(n):
|
|
176
|
+
node_id = n.get('id')
|
|
177
|
+
if node_id:
|
|
178
|
+
service_ids.add(node_id)
|
|
179
|
+
count = 0
|
|
180
|
+
for e in self.edges:
|
|
181
|
+
if not isinstance(e, dict):
|
|
182
|
+
continue
|
|
183
|
+
etype = normalize_edge_type(e.get('type'))
|
|
184
|
+
if edge_types is not None and etype not in edge_types:
|
|
185
|
+
continue
|
|
186
|
+
source, target = e.get('source'), e.get('target')
|
|
187
|
+
if source in service_ids and target in service_ids and source != target:
|
|
188
|
+
count += 1
|
|
189
|
+
return count, len(service_ids)
|
|
190
|
+
|
|
191
|
+
def _count_build_module_dirs(self) -> int:
|
|
192
|
+
"""Count distinct directories that carry a build manifest."""
|
|
193
|
+
manifests = (
|
|
194
|
+
'pom.xml', 'build.gradle', 'build.gradle.kts', 'package.json',
|
|
195
|
+
)
|
|
196
|
+
dirs: set[str] = set()
|
|
197
|
+
for f in self.repo_files:
|
|
198
|
+
if not isinstance(f, str):
|
|
199
|
+
continue
|
|
200
|
+
lower = f.lower().replace('\\', '/')
|
|
201
|
+
name = lower.rsplit('/', 1)[-1]
|
|
202
|
+
if name in manifests:
|
|
203
|
+
parent = lower.rsplit('/', 1)[0] if '/' in lower else '.'
|
|
204
|
+
dirs.add(parent)
|
|
205
|
+
return len(dirs)
|
|
206
|
+
|
|
207
|
+
def apply_graph_consistency_gate(
|
|
208
|
+
self, dimension_scores: dict[str, dict[str, Any]]
|
|
209
|
+
) -> dict[str, Any]:
|
|
210
|
+
"""Reconcile deployment_model with the component graph when they disagree."""
|
|
211
|
+
edge_count, service_count = self._count_inter_service_edges()
|
|
212
|
+
info: dict[str, Any] = {
|
|
213
|
+
'applied': False,
|
|
214
|
+
'service_nodes': service_count,
|
|
215
|
+
'inter_service_edges': edge_count,
|
|
216
|
+
}
|
|
217
|
+
dim = dimension_scores.get('deployment_model')
|
|
218
|
+
if not dim:
|
|
219
|
+
return info
|
|
220
|
+
|
|
221
|
+
current = float(dim.get('score', 0))
|
|
222
|
+
target: float | None = None
|
|
223
|
+
if service_count >= 8 and edge_count >= max(5, service_count // 2):
|
|
224
|
+
if current < 69:
|
|
225
|
+
target = 72.0
|
|
226
|
+
elif service_count >= 4 and edge_count >= 3:
|
|
227
|
+
if current < 47:
|
|
228
|
+
target = 50.0
|
|
229
|
+
|
|
230
|
+
if target is None or target <= current:
|
|
231
|
+
return info
|
|
232
|
+
|
|
233
|
+
info.update({
|
|
234
|
+
'applied': True,
|
|
235
|
+
'original_score': round(current, 1),
|
|
236
|
+
'adjusted_score': round(target, 1),
|
|
237
|
+
})
|
|
238
|
+
dim['score'] = round(target, 1)
|
|
239
|
+
dim['evidence'].append(self._evidence(
|
|
240
|
+
f'Graph consistency gate: {service_count} first-party service '
|
|
241
|
+
f'node(s) with {edge_count} inter-service call edge(s) contradict '
|
|
242
|
+
f'a monolith-band deployment score; re-derived from '
|
|
243
|
+
f'{current:.0f} to {target:.0f}.',
|
|
244
|
+
'strong',
|
|
245
|
+
))
|
|
246
|
+
return info
|
|
89
247
|
|
|
90
248
|
# -- helpers --------------------------------------------------------
|
|
91
249
|
|
|
@@ -269,7 +427,7 @@ class DimensionScorer:
|
|
|
269
427
|
'helm', 'kustomize', '.github/workflows', '.gitlab-ci',
|
|
270
428
|
'jenkinsfile', 'buildspec',
|
|
271
429
|
]
|
|
272
|
-
phase1_files_raw = (
|
|
430
|
+
phase1_files_raw = self.repo_files or (
|
|
273
431
|
self.phase1.get('relevant_files', [])
|
|
274
432
|
or self.phase2.get('phase1_files', [])
|
|
275
433
|
or []
|
|
@@ -366,6 +524,7 @@ class DimensionScorer:
|
|
|
366
524
|
'weak',
|
|
367
525
|
))
|
|
368
526
|
|
|
527
|
+
self._deployable_census = (deployable_count, logical_count)
|
|
369
528
|
return deployable_count, logical_count, evidence
|
|
370
529
|
|
|
371
530
|
@staticmethod
|
|
@@ -1366,9 +1525,43 @@ class TierMatcher:
|
|
|
1366
1525
|
# Tier Gap Analysis
|
|
1367
1526
|
# ---------------------------------------------------------------------------
|
|
1368
1527
|
|
|
1528
|
+
_RECOMMENDATION_EVIDENCE_GUARDS: list[tuple[tuple[str, ...], tuple[str, ...]]] = [
|
|
1529
|
+
(('containerize',), ('docker', 'kubernetes', 'k8s', 'containerd', 'podman')),
|
|
1530
|
+
(('kubernetes',), ('kubernetes', 'k8s')),
|
|
1531
|
+
(('infrastructure as code', 'terraform'), ('terraform', 'pulumi', 'cloudformation', 'ansible', 'bicep', 'cdk')),
|
|
1532
|
+
(('distributed tracing', 'opentelemetry'), ('opentelemetry', 'otel', 'jaeger', 'zipkin', 'tempo', 'lightstep')),
|
|
1533
|
+
(('centralized logging',), ('loki', 'elasticsearch', 'elk', 'fluentd', 'fluent bit', 'splunk', 'graylog')),
|
|
1534
|
+
(('monitoring',), ('prometheus', 'grafana', 'datadog', 'new relic', 'newrelic', 'dynatrace', 'cloudwatch')),
|
|
1535
|
+
(('caching layer', 'application-level caching'), ('redis', 'memcached', 'valkey', 'hazelcast')),
|
|
1536
|
+
(('streaming platform', 'kafka'), ('kafka', 'pulsar', 'kinesis')),
|
|
1537
|
+
(('gitops',), ('argocd', 'argo cd', 'argo', 'fluxcd', 'flux cd')),
|
|
1538
|
+
(('cdn',), ('cloudfront', 'cloudflare', 'akamai', 'fastly')),
|
|
1539
|
+
(('autoscaling policies', 'hpa/keda'), ('hpa', 'keda')),
|
|
1540
|
+
(('ci/cd pipeline',), ('github actions', 'gitlab ci', 'jenkins', 'circleci', 'tekton', 'buildkite')),
|
|
1541
|
+
]
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
def _recommendation_contradicts_evidence(
|
|
1545
|
+
recommendation: str, detected_technologies: list[str]
|
|
1546
|
+
) -> bool:
|
|
1547
|
+
"""True when every capability the recommendation names is already detected."""
|
|
1548
|
+
rec_lower = str(recommendation or '').lower()
|
|
1549
|
+
detected_blob = ' '.join(str(t).lower() for t in (detected_technologies or []))
|
|
1550
|
+
if not rec_lower or not detected_blob:
|
|
1551
|
+
return False
|
|
1552
|
+
matched_any = False
|
|
1553
|
+
for needles, techs in _RECOMMENDATION_EVIDENCE_GUARDS:
|
|
1554
|
+
if any(needle in rec_lower for needle in needles):
|
|
1555
|
+
matched_any = True
|
|
1556
|
+
if not any(tech in detected_blob for tech in techs):
|
|
1557
|
+
return False
|
|
1558
|
+
return matched_any
|
|
1559
|
+
|
|
1560
|
+
|
|
1369
1561
|
def compute_tier_gap_analysis(
|
|
1370
1562
|
primary_tier: int,
|
|
1371
1563
|
dimension_scores: dict[str, dict[str, Any]],
|
|
1564
|
+
detected_technologies: list[str] | None = None,
|
|
1372
1565
|
) -> dict[str, Any]:
|
|
1373
1566
|
"""Describe what is needed to advance from the current tier to the next."""
|
|
1374
1567
|
if primary_tier >= 5:
|
|
@@ -1396,16 +1589,29 @@ def compute_tier_gap_analysis(
|
|
|
1396
1589
|
}
|
|
1397
1590
|
else:
|
|
1398
1591
|
recs = td.GAP_RECOMMENDATIONS.get(dim_key, {})
|
|
1592
|
+
recommendation = recs.get(
|
|
1593
|
+
primary_tier,
|
|
1594
|
+
f'Advance {td.DIMENSION_LABELS.get(dim_key, dim_key)} '
|
|
1595
|
+
f'capabilities toward Tier {next_tier}.',
|
|
1596
|
+
)
|
|
1597
|
+
recommendation_adjusted = False
|
|
1598
|
+
if _recommendation_contradicts_evidence(
|
|
1599
|
+
recommendation, detected_technologies or []
|
|
1600
|
+
):
|
|
1601
|
+
recommendation = (
|
|
1602
|
+
f'Core tooling for this step is already in place; deepen '
|
|
1603
|
+
f'its adoption and coverage to raise '
|
|
1604
|
+
f'{td.DIMENSION_LABELS.get(dim_key, dim_key)} toward '
|
|
1605
|
+
f'Tier {next_tier}.'
|
|
1606
|
+
)
|
|
1607
|
+
recommendation_adjusted = True
|
|
1399
1608
|
actions[dim_key] = {
|
|
1400
1609
|
'status': 'below_target',
|
|
1401
1610
|
'current_score': current_score,
|
|
1402
1611
|
'target_score': target,
|
|
1403
1612
|
'gap': round(gap, 1),
|
|
1404
|
-
'recommendation':
|
|
1405
|
-
|
|
1406
|
-
f'Advance {td.DIMENSION_LABELS.get(dim_key, dim_key)} '
|
|
1407
|
-
f'capabilities toward Tier {next_tier}.',
|
|
1408
|
-
),
|
|
1613
|
+
'recommendation': recommendation,
|
|
1614
|
+
'recommendation_adjusted': recommendation_adjusted,
|
|
1409
1615
|
}
|
|
1410
1616
|
|
|
1411
1617
|
return {
|
|
@@ -1450,6 +1656,20 @@ def classify_local(
|
|
|
1450
1656
|
repo_kind_value = rk.classify_repo_kind(scorer.detected_techs, scorer.detected_patterns)
|
|
1451
1657
|
tier_not_applicable = repo_kind_value in rk.ABSTAIN_REPO_KINDS
|
|
1452
1658
|
|
|
1659
|
+
consistency_gate = scorer.apply_graph_consistency_gate(dimension_scores)
|
|
1660
|
+
|
|
1661
|
+
deployable_units, logical_units = scorer._deployable_census or (0, 0)
|
|
1662
|
+
inter_service_edges, service_node_count = scorer._count_inter_service_edges()
|
|
1663
|
+
architecture_style = derive_architecture_style(
|
|
1664
|
+
deployable_units=deployable_units,
|
|
1665
|
+
logical_units=logical_units,
|
|
1666
|
+
service_node_count=service_node_count,
|
|
1667
|
+
inter_service_edge_count=inter_service_edges,
|
|
1668
|
+
detected_patterns=scorer.detected_patterns,
|
|
1669
|
+
repo_kind=repo_kind_value,
|
|
1670
|
+
module_dirs=scorer._count_build_module_dirs(),
|
|
1671
|
+
)
|
|
1672
|
+
|
|
1453
1673
|
match_result = TierMatcher.match(dimension_scores)
|
|
1454
1674
|
|
|
1455
1675
|
if tier_not_applicable:
|
|
@@ -1465,6 +1685,7 @@ def classify_local(
|
|
|
1465
1685
|
gap_analysis = compute_tier_gap_analysis(
|
|
1466
1686
|
match_result['primary_tier'],
|
|
1467
1687
|
match_result['dimension_scores'],
|
|
1688
|
+
detected_technologies=scorer.detected_techs,
|
|
1468
1689
|
)
|
|
1469
1690
|
|
|
1470
1691
|
elapsed = round(time.time() - start_time, 2)
|
|
@@ -1499,4 +1720,9 @@ def classify_local(
|
|
|
1499
1720
|
'confidence_components': match_result.get('confidence_components', {}),
|
|
1500
1721
|
'penalty_caps_applied': getattr(scorer, '_applied_penalty_caps', []),
|
|
1501
1722
|
'penalty_signals': scorer.penalty_signals,
|
|
1723
|
+
'architecture_style': architecture_style,
|
|
1724
|
+
'architecture_style_label': ARCHITECTURE_STYLE_LABELS.get(
|
|
1725
|
+
architecture_style, architecture_style.replace('_', ' ').title()
|
|
1726
|
+
),
|
|
1727
|
+
'graph_consistency_gate': consistency_gate,
|
|
1502
1728
|
}
|