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.
Files changed (107) hide show
  1. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/PKG-INFO +2 -2
  2. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/__init__.py +1 -1
  3. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/archetype_classifier.py +232 -6
  4. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/architecture_analyzer.py +368 -73
  5. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/MANIFEST.json +8 -4
  6. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/enhanced_technology_detector.py +81 -3
  7. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/repo_kind.py +5 -0
  8. codedd_cli-0.1.9/codedd_cli/auditor/server_parity/taxonomy.py +500 -0
  9. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/cli.py +3 -2
  10. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/audit_cmd.py +5 -3
  11. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/scope_cmd.py +86 -6
  12. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/pyproject.toml +3 -2
  13. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/LICENSE +0 -0
  14. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/README.md +0 -0
  15. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/__main__.py +0 -0
  16. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/api/__init__.py +0 -0
  17. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/api/client.py +0 -0
  18. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/api/endpoints.py +0 -0
  19. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/api/exceptions.py +0 -0
  20. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/__init__.py +0 -0
  21. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/architecture_prompts.py +0 -0
  22. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/architecture_schema_contract.py +0 -0
  23. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/audit_checkpoint.py +0 -0
  24. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/audit_progress_sync.py +0 -0
  25. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/commit_classifier.py +0 -0
  26. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/complexity_analyzer.py +0 -0
  27. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/cross_repo_evidence.py +0 -0
  28. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/dependency_scanner.py +0 -0
  29. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/file_auditor.py +0 -0
  30. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/git_stats_collector.py +0 -0
  31. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/parity_overrides.py +0 -0
  32. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/response_parser.py +0 -0
  33. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/__init__.py +0 -0
  34. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/enhanced_relationship_detector.py +0 -0
  35. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/evidence_extractor.py +0 -0
  36. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/infra_scan.py +0 -0
  37. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/manifest_coordinate_resolver.py +0 -0
  38. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/published_identity.py +0 -0
  39. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/repo_file.py +0 -0
  40. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/tech_normalizer.py +0 -0
  41. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/technology_lists.py +0 -0
  42. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/server_parity/tier_definitions.py +0 -0
  43. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/sub_audit_runner.py +0 -0
  44. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/tier_definitions.py +0 -0
  45. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/__init__.py +0 -0
  46. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/agents.py +0 -0
  47. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/cli_ai_auditor.py +0 -0
  48. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/cli_orchestrator.py +0 -0
  49. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/cli_settings.py +0 -0
  50. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/file_io.py +0 -0
  51. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/__init__.py +0 -0
  52. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/_generic.py +0 -0
  53. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/c_cpp.py +0 -0
  54. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/csharp.py +0 -0
  55. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/go.py +0 -0
  56. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/java.py +0 -0
  57. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/js_ts.py +0 -0
  58. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/kotlin.py +0 -0
  59. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/php.py +0 -0
  60. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/ruby.py +0 -0
  61. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/rust.py +0 -0
  62. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/scala.py +0 -0
  63. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/shell.py +0 -0
  64. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/swift.py +0 -0
  65. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_ast/taint.py +0 -0
  66. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/lang_snippets.py +0 -0
  67. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/langgraph_runner.py +0 -0
  68. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/planner.py +0 -0
  69. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/prompts.py +0 -0
  70. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/scope.py +0 -0
  71. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/security_patterns.py +0 -0
  72. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/semgrep_runner.py +0 -0
  73. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/state.py +0 -0
  74. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_analysis/tools.py +0 -0
  75. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auditor/vulnerability_validator.py +0 -0
  76. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auth/__init__.py +0 -0
  77. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auth/session.py +0 -0
  78. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/auth/token_manager.py +0 -0
  79. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/__init__.py +0 -0
  80. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/ai_docs_cmd.py +0 -0
  81. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/audits_cmd.py +0 -0
  82. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/auth_cmd.py +0 -0
  83. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/config_cmd.py +0 -0
  84. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/commands/fix_cmd.py +0 -0
  85. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/config/__init__.py +0 -0
  86. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/config/constants.py +0 -0
  87. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/config/fix_session.py +0 -0
  88. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/config/settings.py +0 -0
  89. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/config/url_validation.py +0 -0
  90. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/llm/__init__.py +0 -0
  91. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/llm/anthropic_utils.py +0 -0
  92. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/llm/key_manager.py +0 -0
  93. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/models/__init__.py +0 -0
  94. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/models/account.py +0 -0
  95. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/models/audit.py +0 -0
  96. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/models/local_directory.py +0 -0
  97. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/scanner/__init__.py +0 -0
  98. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/scanner/file_classifier.py +0 -0
  99. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/scanner/file_walker.py +0 -0
  100. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/scanner/line_counter.py +0 -0
  101. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/utils/__init__.py +0 -0
  102. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/utils/directory_validator.py +0 -0
  103. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/utils/display.py +0 -0
  104. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/utils/payload_inspector.py +0 -0
  105. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/utils/safe_path.py +0 -0
  106. {codedd_cli-0.1.8 → codedd_cli-0.1.9}/codedd_cli/utils/security.py +0 -0
  107. {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.8
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[all] (>=0.9.0)
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
@@ -1,3 +1,3 @@
1
1
  """CodeDD CLI — run code audits from your terminal."""
2
2
 
3
- __version__ = "0.1.8"
3
+ __version__ = "0.1.9"
@@ -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': recs.get(
1405
- primary_tier,
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
  }