architec 0.2.11__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 (191) hide show
  1. architec/__init__.py +52 -0
  2. architec/__main__.py +4 -0
  3. architec/_compat_reexport.py +20 -0
  4. architec/_version.py +4 -0
  5. architec/advice_feedback.py +263 -0
  6. architec/analysis/__init__.py +5 -0
  7. architec/analysis/analysis_cache.py +76 -0
  8. architec/analysis/analysis_runner_flow.py +208 -0
  9. architec/analysis/analysis_runner_llm.py +47 -0
  10. architec/analysis/analysis_runner_recommendations.py +104 -0
  11. architec/analysis/analysis_runner_report.py +174 -0
  12. architec/analysis/analysis_runner_report_support.py +104 -0
  13. architec/analysis/analysis_runner_report_views.py +188 -0
  14. architec/analysis/governance_dimensions.py +195 -0
  15. architec/analysis/history_analyzer.py +198 -0
  16. architec/analysis/history_analyzer_report.py +96 -0
  17. architec/analysis/hotspot_digest.py +62 -0
  18. architec/analysis/hotspot_digest_rank.py +150 -0
  19. architec/analysis/hotspot_digest_sources.py +156 -0
  20. architec/analysis/public.py +234 -0
  21. architec/analysis/repo_topology.py +235 -0
  22. architec/analysis/repo_topology_findings.py +151 -0
  23. architec/analysis/repo_topology_group_terms.py +199 -0
  24. architec/analysis/repo_topology_groups.py +168 -0
  25. architec/analysis/repo_topology_llm.py +192 -0
  26. architec/analysis/repo_topology_migration.py +266 -0
  27. architec/analysis/repo_topology_migration_helpers.py +247 -0
  28. architec/analysis/repo_topology_paths.py +123 -0
  29. architec/analysis/repo_topology_review_helpers.py +204 -0
  30. architec/analysis/repo_topology_rule_data.py +121 -0
  31. architec/analysis/repo_topology_rules.py +119 -0
  32. architec/analysis_runner.py +3 -0
  33. architec/auth/__init__.py +21 -0
  34. architec/auth/client.py +149 -0
  35. architec/auth/commands.py +658 -0
  36. architec/auth/device.py +36 -0
  37. architec/auth/guard.py +65 -0
  38. architec/auth/lease.py +77 -0
  39. architec/auth/store.py +78 -0
  40. architec/backend_llm/__init__.py +177 -0
  41. architec/backend_llm/cache.py +143 -0
  42. architec/backend_llm/config.py +370 -0
  43. architec/backend_llm/errors.py +20 -0
  44. architec/backend_llm/failover.py +158 -0
  45. architec/backend_llm/flow.py +401 -0
  46. architec/backend_llm/gateway.py +160 -0
  47. architec/backend_llm/parse.py +144 -0
  48. architec/baseline/__init__.py +11 -0
  49. architec/baseline/report.py +227 -0
  50. architec/cleanup/__init__.py +67 -0
  51. architec/cleanup/archive.py +205 -0
  52. architec/cleanup/autofix.py +258 -0
  53. architec/cleanup/inventory.py +229 -0
  54. architec/cleanup/metadata.py +46 -0
  55. architec/cleanup/report.py +119 -0
  56. architec/cleanup/retire_plan.py +380 -0
  57. architec/cleanup/scope.py +134 -0
  58. architec/cleanup/semantic_judge.py +451 -0
  59. architec/cli.py +973 -0
  60. architec/code_review/__init__.py +9 -0
  61. architec/code_review/architecture_contracts.py +190 -0
  62. architec/code_review/near_duplicate.py +700 -0
  63. architec/code_review/plan_diff_consistency.py +1201 -0
  64. architec/code_review/public.py +2156 -0
  65. architec/code_review/python_imports.py +96 -0
  66. architec/code_review/risk_context.py +376 -0
  67. architec/code_review/scan_cache.py +112 -0
  68. architec/code_review/shadow_implementation.py +1261 -0
  69. architec/component_descriptors.py +3 -0
  70. architec/descriptors/__init__.py +17 -0
  71. architec/descriptors/component_descriptors_builder.py +104 -0
  72. architec/descriptors/component_descriptors_semantics.py +19 -0
  73. architec/descriptors/component_descriptors_semantics_roles.py +75 -0
  74. architec/descriptors/component_descriptors_semantics_terms.py +95 -0
  75. architec/descriptors/component_descriptors_symbols.py +82 -0
  76. architec/descriptors/component_graph.py +108 -0
  77. architec/descriptors/public.py +15 -0
  78. architec/events/__init__.py +3 -0
  79. architec/events/public.py +132 -0
  80. architec/feature/__init__.py +5 -0
  81. architec/feature/feature_advisor.py +130 -0
  82. architec/feature/feature_advisor_llm.py +55 -0
  83. architec/feature/feature_advisor_ranking.py +88 -0
  84. architec/feature/feature_advisor_ranking_output.py +55 -0
  85. architec/feature/feature_advisor_ranking_phase1.py +95 -0
  86. architec/feature/feature_advisor_ranking_phase2.py +91 -0
  87. architec/feature/feature_advisor_targets.py +159 -0
  88. architec/feature/feature_query.py +128 -0
  89. architec/feature/feature_query_scoring.py +120 -0
  90. architec/fix_advice/__init__.py +3 -0
  91. architec/fix_advice/public.py +518 -0
  92. architec/gate/__init__.py +13 -0
  93. architec/gate/report.py +265 -0
  94. architec/integration/__init__.py +1 -0
  95. architec/integration/bundle_loader.py +287 -0
  96. architec/integration/hippo_adapter.py +181 -0
  97. architec/integration/hippo_adapter_paths.py +177 -0
  98. architec/integration/hippo_adapter_snapshot.py +27 -0
  99. architec/integration/hippo_bridge.py +101 -0
  100. architec/integration/paths.py +35 -0
  101. architec/integration/resource_paths.py +131 -0
  102. architec/orchestrator/__init__.py +194 -0
  103. architec/orchestrator/component_qa.py +189 -0
  104. architec/orchestrator/component_qa_selection.py +180 -0
  105. architec/orchestrator/orchestrator_batch_builders.py +168 -0
  106. architec/orchestrator/orchestrator_batch_helpers.py +135 -0
  107. architec/orchestrator/orchestrator_batches.py +119 -0
  108. architec/orchestrator/orchestrator_flow.py +152 -0
  109. architec/orchestrator/orchestrator_llm.py +39 -0
  110. architec/orchestrator/orchestrator_report.py +105 -0
  111. architec/orchestrator/orchestrator_test_plan.py +495 -0
  112. architec/orchestrator/orchestrator_timing.py +11 -0
  113. architec/plan_review/__init__.py +7 -0
  114. architec/plan_review/public.py +160 -0
  115. architec/project_status/__init__.py +3 -0
  116. architec/project_status/public.py +117 -0
  117. architec/reporting/__init__.py +1 -0
  118. architec/reporting/architecture_report_compaction.py +172 -0
  119. architec/reporting/architecture_report_md.py +160 -0
  120. architec/reporting/architecture_report_sections.py +181 -0
  121. architec/reporting/report_markdown.py +360 -0
  122. architec/reporting/viz_generator.py +157 -0
  123. architec/reporting/viz_generator_cards.py +59 -0
  124. architec/reporting/viz_generator_sections.py +43 -0
  125. architec/reporting/viz_generator_view.py +288 -0
  126. architec/scoring/__init__.py +19 -0
  127. architec/scoring/component_scoring.py +136 -0
  128. architec/scoring/component_scoring_git.py +75 -0
  129. architec/scoring/component_scoring_llm.py +35 -0
  130. architec/scoring/component_scoring_payload.py +44 -0
  131. architec/scoring/component_scoring_registry.py +93 -0
  132. architec/scoring/component_scoring_runtime.py +182 -0
  133. architec/scoring/component_scoring_scope.py +50 -0
  134. architec/scoring/component_selection_policy.py +58 -0
  135. architec/scoring/contract_engine.py +142 -0
  136. architec/scoring/public.py +50 -0
  137. architec/scoring/scoring_policy_common.py +68 -0
  138. architec/scoring/scoring_policy_defaults.py +76 -0
  139. architec/scoring/scoring_policy_full_eval.py +208 -0
  140. architec/scoring/scoring_policy_incremental_eval.py +103 -0
  141. architec/scoring/scoring_policy_incremental_helpers.py +79 -0
  142. architec/scoring/scoring_policy_incremental_reasoning.py +73 -0
  143. architec/scoring/scoring_policy_overall_eval.py +146 -0
  144. architec/scoring_policy.py +3 -0
  145. architec/self_manage.py +296 -0
  146. architec/support/__init__.py +1 -0
  147. architec/support/architecture_rules.py +376 -0
  148. architec/support/io_utils.py +45 -0
  149. architec/support/llm_guard.py +88 -0
  150. architec/support/llm_preflight.py +128 -0
  151. architec/support/path_policy.py +326 -0
  152. architec/support/refresh_decider.py +241 -0
  153. architec/support/refresh_decider_git.py +105 -0
  154. architec/support/tls.py +42 -0
  155. architec/version.py +44 -0
  156. architec-0.2.11.data/data/architec/config/config.default.yaml +38 -0
  157. architec-0.2.11.data/data/architec/config/config.example.yaml +38 -0
  158. architec-0.2.11.data/data/architec/config/rubric.json +76 -0
  159. architec-0.2.11.data/data/architec/config/scoring-policy.json +86 -0
  160. architec-0.2.11.data/data/architec/prompts/analyze.md +82 -0
  161. architec-0.2.11.data/data/architec/prompts/codex-repair.md +36 -0
  162. architec-0.2.11.data/data/architec/prompts/component-qa.md +10 -0
  163. architec-0.2.11.data/data/architec/prompts/component-scoring.md +17 -0
  164. architec-0.2.11.data/data/architec/prompts/feature-architecture.md +13 -0
  165. architec-0.2.11.data/data/architec/prompts/folder-naming-judge.md +129 -0
  166. architec-0.2.11.data/data/architec/prompts/full-report.md +14 -0
  167. architec-0.2.11.data/data/architec/prompts/history-remediation.md +13 -0
  168. architec-0.2.11.data/data/architec/prompts/orchestrator-program.md +14 -0
  169. architec-0.2.11.data/data/architec/prompts/review-diff.md +40 -0
  170. architec-0.2.11.data/data/architec/prompts/semantic-judge.md +69 -0
  171. architec-0.2.11.data/data/architec/prompts/split-expert.md +30 -0
  172. architec-0.2.11.data/data/architec/prompts/summary.md +14 -0
  173. architec-0.2.11.data/data/architec/prompts/system.md +60 -0
  174. architec-0.2.11.data/data/architec/prompts/tool-orchestrator.md +65 -0
  175. architec-0.2.11.data/data/architec/prompts/topology-review-judge.md +106 -0
  176. architec-0.2.11.data/data/architec/tools/archi_entry.py +12 -0
  177. architec-0.2.11.data/data/architec/tools/build_architect_prompt.py +223 -0
  178. architec-0.2.11.data/data/architec/tools/collect_repo_metrics.py +282 -0
  179. architec-0.2.11.data/data/architec/tools/collect_repo_metrics_python.py +187 -0
  180. architec-0.2.11.data/data/architec/tools/collect_repo_metrics_rules.py +26 -0
  181. architec-0.2.11.data/data/architec/tools/collect_repo_metrics_runtime.py +92 -0
  182. architec-0.2.11.data/data/architec/tools/collect_repo_metrics_scan.py +141 -0
  183. architec-0.2.11.data/data/architec/tools/prod_browser_auth_smoke.sh +390 -0
  184. architec-0.2.11.data/data/architec/tools/refresh_architect_context.sh +16 -0
  185. architec-0.2.11.data/data/architec/tools/run_archi_via_auth_tunnel.sh +141 -0
  186. architec-0.2.11.dist-info/METADATA +322 -0
  187. architec-0.2.11.dist-info/RECORD +191 -0
  188. architec-0.2.11.dist-info/WHEEL +5 -0
  189. architec-0.2.11.dist-info/entry_points.txt +2 -0
  190. architec-0.2.11.dist-info/licenses/LICENSE +21 -0
  191. architec-0.2.11.dist-info/top_level.txt +1 -0
architec/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ """Architec analysis engine.
2
+
3
+ Independent from runtime proxy internals; integrates with Hippos via
4
+ read-only `.hippos` artifacts and writes its own outputs to `.architec`.
5
+ """
6
+
7
+ __all__ = [
8
+ "run_analysis",
9
+ "analyze_history_and_iterate",
10
+ "suggest_feature_architecture",
11
+ "score_changed_components",
12
+ "load_scoring_policy",
13
+ "evaluate_full_score",
14
+ "evaluate_incremental_score",
15
+ "evaluate_overall_score",
16
+ ]
17
+
18
+
19
+ def __getattr__(name: str):
20
+ if name == "run_analysis":
21
+ from .analysis.public import run_analysis
22
+
23
+ return run_analysis
24
+ if name == "analyze_history_and_iterate":
25
+ from .analysis.history_analyzer import analyze_history_and_iterate
26
+
27
+ return analyze_history_and_iterate
28
+ if name == "suggest_feature_architecture":
29
+ from .feature.feature_advisor import suggest_feature_architecture
30
+
31
+ return suggest_feature_architecture
32
+ if name == "score_changed_components":
33
+ from .scoring.component_scoring import score_changed_components
34
+
35
+ return score_changed_components
36
+ if name == "load_scoring_policy":
37
+ from .scoring.public import load_scoring_policy
38
+
39
+ return load_scoring_policy
40
+ if name == "evaluate_full_score":
41
+ from .scoring.public import evaluate_full_score
42
+
43
+ return evaluate_full_score
44
+ if name == "evaluate_incremental_score":
45
+ from .scoring.public import evaluate_incremental_score
46
+
47
+ return evaluate_incremental_score
48
+ if name == "evaluate_overall_score":
49
+ from .scoring.public import evaluate_overall_score
50
+
51
+ return evaluate_overall_score
52
+ raise AttributeError(f"module 'architec' has no attribute {name!r}")
architec/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from importlib import import_module
5
+ from types import ModuleType
6
+ from typing import Any
7
+
8
+
9
+ def reexport(package_name: str, target: str, namespace: dict[str, Any]) -> ModuleType:
10
+ module_name = str(namespace.get("__name__", package_name) or package_name)
11
+ target_module = import_module(target, package_name)
12
+ sys.modules[module_name] = target_module
13
+
14
+ parent_name, _, child_name = module_name.rpartition(".")
15
+ if parent_name and child_name:
16
+ parent_module = sys.modules.get(parent_name)
17
+ if parent_module is not None:
18
+ setattr(parent_module, child_name, target_module)
19
+
20
+ return target_module
architec/_version.py ADDED
@@ -0,0 +1,4 @@
1
+ from __future__ import annotations
2
+
3
+ # Package-local version fallback for bundled/compiled runtime environments.
4
+ __version__ = "0.2.11"
@@ -0,0 +1,263 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ DEMOTE_STATUSES = {"rejected", "not_applicable", "superseded"}
10
+ KNOWN_STATUSES = DEMOTE_STATUSES | {"accepted", "deferred"}
11
+ KNOWN_SCOPES = {"exact_advice", "same_path_kind", "pattern"}
12
+
13
+
14
+ class AdviceFeedbackInputError(RuntimeError):
15
+ """Raised when advice feedback JSON cannot be read or used."""
16
+
17
+
18
+ def _dict(value: object) -> dict[str, Any]:
19
+ return value if isinstance(value, dict) else {}
20
+
21
+
22
+ def _list(value: object) -> list[Any]:
23
+ return value if isinstance(value, list) else []
24
+
25
+
26
+ def _clean(value: object) -> str:
27
+ return str(value or "").strip()
28
+
29
+
30
+ def _norm_token(value: object) -> str:
31
+ return _clean(value).lower().replace("-", "_").replace(" ", "_")
32
+
33
+
34
+ def _norm_path(value: object) -> str:
35
+ return _clean(value).replace("\\", "/").lstrip("./")
36
+
37
+
38
+ def _stable_advice_id(prefix: str, payload: dict[str, Any]) -> str:
39
+ encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
40
+ return f"{prefix}:{hashlib.sha256(encoded.encode('utf-8')).hexdigest()[:12]}"
41
+
42
+
43
+ def _feedback_items(feedback: dict[str, Any] | None) -> list[dict[str, Any]]:
44
+ if not isinstance(feedback, dict):
45
+ return []
46
+ out: list[dict[str, Any]] = []
47
+ for item in _list(feedback.get("items")):
48
+ if not isinstance(item, dict):
49
+ continue
50
+ status = _norm_token(item.get("status"))
51
+ if status not in KNOWN_STATUSES:
52
+ continue
53
+ scope = _norm_token(item.get("scope"))
54
+ if scope not in KNOWN_SCOPES:
55
+ if _clean(item.get("advice_id")) or _clean(item.get("concern_id")):
56
+ scope = "exact_advice"
57
+ elif _clean(item.get("path")):
58
+ scope = "same_path_kind"
59
+ elif _clean(item.get("pattern")):
60
+ scope = "pattern"
61
+ else:
62
+ continue
63
+ out.append(
64
+ {
65
+ "advice_id": _clean(item.get("advice_id")),
66
+ "concern_id": _clean(item.get("concern_id")),
67
+ "kind": _norm_token(item.get("kind")),
68
+ "path": _norm_path(item.get("path")),
69
+ "symbol": _clean(item.get("symbol")),
70
+ "status": status,
71
+ "scope": scope,
72
+ "pattern": _clean(item.get("pattern")).lower(),
73
+ "reason": _clean(item.get("reason")),
74
+ }
75
+ )
76
+ return out
77
+
78
+
79
+ def load_advice_feedback(path: str | Path) -> dict[str, Any]:
80
+ source = Path(path)
81
+ try:
82
+ raw = source.read_text(encoding="utf-8")
83
+ except FileNotFoundError as exc:
84
+ raise AdviceFeedbackInputError(f"Advice feedback JSON not found: {source}") from exc
85
+ except OSError as exc:
86
+ raise AdviceFeedbackInputError(f"Unable to read advice feedback JSON: {source}: {exc}") from exc
87
+ try:
88
+ data = json.loads(raw)
89
+ except json.JSONDecodeError as exc:
90
+ raise AdviceFeedbackInputError(f"Invalid advice feedback JSON: {source}: {exc.msg}") from exc
91
+ if not isinstance(data, dict):
92
+ raise AdviceFeedbackInputError(f"Advice feedback JSON must be an object: {source}")
93
+ items = data.get("items")
94
+ if items is not None and not isinstance(items, list):
95
+ raise AdviceFeedbackInputError(f"Advice feedback items must be a list: {source}")
96
+ data = dict(data)
97
+ data["_source_path"] = str(source)
98
+ return data
99
+
100
+
101
+ def recommendation_target(item: dict[str, Any]) -> dict[str, Any]:
102
+ title = _clean(item.get("title"))
103
+ scope = _clean(item.get("scope"))
104
+ why = _clean(item.get("why"))
105
+ priority = _clean(item.get("priority"))
106
+ kind = _norm_token(item.get("kind")) or "recommendation"
107
+ advice_id = _clean(item.get("advice_id")) or _stable_advice_id(
108
+ "archi-advice:recommendation",
109
+ {
110
+ "kind": kind,
111
+ "priority": priority,
112
+ "title": title,
113
+ "scope": scope,
114
+ "why": why,
115
+ },
116
+ )
117
+ return {
118
+ "advice_id": advice_id,
119
+ "kind": kind,
120
+ "path": _norm_path(item.get("path")) or _norm_path(title) or _norm_path(scope),
121
+ "title": title,
122
+ "scope": scope,
123
+ "why": why,
124
+ "text": " ".join([advice_id, kind, title, scope, why]).lower(),
125
+ }
126
+
127
+
128
+ def concern_target(concern: dict[str, Any]) -> dict[str, Any]:
129
+ location = _dict(concern.get("location"))
130
+ concern_id = _clean(concern.get("concern_id"))
131
+ kind = _norm_token(concern.get("kind"))
132
+ path = _norm_path(location.get("path"))
133
+ symbol = _clean(location.get("symbol"))
134
+ return {
135
+ "advice_id": "",
136
+ "concern_id": concern_id,
137
+ "kind": kind,
138
+ "path": path,
139
+ "symbol": symbol,
140
+ "title": "",
141
+ "scope": path,
142
+ "why": "",
143
+ "text": " ".join([concern_id, kind, path, symbol]).lower(),
144
+ }
145
+
146
+
147
+ def _path_matches(entry_path: str, target: dict[str, Any]) -> bool:
148
+ if not entry_path:
149
+ return False
150
+ target_path = _norm_path(target.get("path"))
151
+ if target_path == entry_path:
152
+ return True
153
+ haystack = " ".join(
154
+ [
155
+ _norm_path(target.get("path")),
156
+ _clean(target.get("title")),
157
+ _clean(target.get("scope")),
158
+ _clean(target.get("text")),
159
+ ]
160
+ ).lower()
161
+ return entry_path.lower() in haystack
162
+
163
+
164
+ def _kind_matches(entry_kind: str, target: dict[str, Any]) -> bool:
165
+ if not entry_kind:
166
+ return True
167
+ target_kind = _norm_token(target.get("kind"))
168
+ if target_kind == entry_kind:
169
+ return True
170
+ return entry_kind in _clean(target.get("text")).lower()
171
+
172
+
173
+ def demoting_feedback_for_target(
174
+ target: dict[str, Any],
175
+ feedback: dict[str, Any] | None,
176
+ ) -> dict[str, Any]:
177
+ target_advice_id = _clean(target.get("advice_id"))
178
+ target_concern_id = _clean(target.get("concern_id"))
179
+ target_text = _clean(target.get("text")).lower()
180
+ for item in _feedback_items(feedback):
181
+ if item["status"] not in DEMOTE_STATUSES:
182
+ continue
183
+ if item["scope"] == "exact_advice":
184
+ if item["advice_id"] and item["advice_id"] == target_advice_id:
185
+ return item
186
+ if item["concern_id"] and item["concern_id"] == target_concern_id:
187
+ return item
188
+ continue
189
+ if item["scope"] == "same_path_kind":
190
+ if _path_matches(item["path"], target) and _kind_matches(item["kind"], target):
191
+ return item
192
+ continue
193
+ if item["scope"] == "pattern":
194
+ pattern = item["pattern"]
195
+ if pattern and pattern in target_text and _kind_matches(item["kind"], target):
196
+ return item
197
+ return {}
198
+
199
+
200
+ def apply_feedback_to_recommendations(
201
+ recommendations: list[dict[str, Any]],
202
+ feedback: dict[str, Any] | None,
203
+ ) -> tuple[list[dict[str, Any]], dict[str, Any]]:
204
+ items = _feedback_items(feedback)
205
+ if not items:
206
+ return recommendations, {}
207
+ kept: list[dict[str, Any]] = []
208
+ demoted: list[dict[str, Any]] = []
209
+ for index, item in enumerate(recommendations):
210
+ if not isinstance(item, dict):
211
+ continue
212
+ target = recommendation_target(item)
213
+ enriched = dict(item)
214
+ enriched.setdefault("advice_id", target["advice_id"])
215
+ match = demoting_feedback_for_target(target, feedback)
216
+ if match:
217
+ demoted.append(
218
+ {
219
+ "advice_id": target["advice_id"],
220
+ "kind": target["kind"],
221
+ "title": target["title"],
222
+ "scope": target["scope"],
223
+ "status": match["status"],
224
+ "feedback_scope": match["scope"],
225
+ "reason": match["reason"],
226
+ "position": index,
227
+ }
228
+ )
229
+ continue
230
+ kept.append(enriched)
231
+ return kept, {
232
+ "input_path": _clean(_dict(feedback).get("_source_path")),
233
+ "item_total": len(items),
234
+ "demoted_recommendation_total": len(demoted),
235
+ "demoted_recommendations": demoted,
236
+ }
237
+
238
+
239
+ def feedback_summary_for_concern(
240
+ concern: dict[str, Any],
241
+ feedback: dict[str, Any] | None,
242
+ ) -> dict[str, Any]:
243
+ target = concern_target(concern)
244
+ match = demoting_feedback_for_target(target, feedback)
245
+ if not match:
246
+ return {}
247
+ return {
248
+ "concern_id": target["concern_id"],
249
+ "kind": target["kind"],
250
+ "path": target["path"],
251
+ "symbol": target["symbol"],
252
+ "status": match["status"],
253
+ "feedback_scope": match["scope"],
254
+ "reason": match["reason"],
255
+ }
256
+
257
+
258
+ __all__ = [
259
+ "AdviceFeedbackInputError",
260
+ "apply_feedback_to_recommendations",
261
+ "feedback_summary_for_concern",
262
+ "load_advice_feedback",
263
+ ]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from .public import run_analysis
4
+
5
+ __all__ = ["run_analysis"]
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from typing import Any
6
+
7
+ from architec.support.io_utils import read_json, utc_now_iso, write_json
8
+ from architec.integration.paths import ANALYSIS_CACHE_DIR
9
+
10
+
11
+ CACHE_DIR = ANALYSIS_CACHE_DIR
12
+
13
+
14
+ def _stable_json(value: object) -> str:
15
+ return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(',', ':'))
16
+
17
+
18
+ def fingerprint_payload(payload: object) -> str:
19
+ return hashlib.sha256(_stable_json(payload).encode('utf-8')).hexdigest()
20
+
21
+
22
+ def _cache_path(root: Path, namespace: str) -> Path:
23
+ safe = ''.join(ch if ch.isalnum() or ch in {'-', '_'} else '_' for ch in namespace)
24
+ return root / CACHE_DIR / f'{safe}.json'
25
+
26
+
27
+ def load_cached_analysis(root: Path, *, namespace: str, payload: object) -> dict[str, Any] | None:
28
+ path = _cache_path(root, namespace)
29
+ data = read_json(path, default={})
30
+ if not isinstance(data, dict):
31
+ return None
32
+ if str(data.get('fingerprint', '') or '') != fingerprint_payload(payload):
33
+ return None
34
+ result = data.get('result')
35
+ if not isinstance(result, dict):
36
+ return None
37
+ cached = dict(result)
38
+ cached['_cache_hit'] = True
39
+ cached['_cache_namespace'] = namespace
40
+ return cached
41
+
42
+
43
+ def save_cached_analysis(
44
+ root: Path,
45
+ *,
46
+ namespace: str,
47
+ payload: object,
48
+ result: dict[str, Any],
49
+ ) -> None:
50
+ path = _cache_path(root, namespace)
51
+ path.parent.mkdir(parents=True, exist_ok=True)
52
+ write_json(
53
+ path,
54
+ {
55
+ 'generated_at': utc_now_iso(),
56
+ 'namespace': namespace,
57
+ 'fingerprint': fingerprint_payload(payload),
58
+ 'result': result,
59
+ },
60
+ )
61
+
62
+
63
+ def run_cached_analysis(
64
+ root: Path,
65
+ *,
66
+ namespace: str,
67
+ payload: object,
68
+ runner,
69
+ ) -> tuple[dict[str, Any] | None, bool]:
70
+ cached = load_cached_analysis(root, namespace=namespace, payload=payload)
71
+ if cached is not None:
72
+ return cached, True
73
+ result = runner()
74
+ if isinstance(result, dict):
75
+ save_cached_analysis(root, namespace=namespace, payload=payload, result=result)
76
+ return result, False
@@ -0,0 +1,208 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from architec.analysis.analysis_runner_llm import (
7
+ llm_summary,
8
+ run_diff_analysis,
9
+ run_goal_analysis,
10
+ )
11
+ from architec.analysis.governance_dimensions import governance_dimensions
12
+ from architec.analysis.analysis_runner_recommendations import (
13
+ recommendations,
14
+ topology_recommendations,
15
+ )
16
+ from architec.support.io_utils import clamp
17
+
18
+
19
+ def score_from_keywords(
20
+ source: dict[str, Any],
21
+ keywords: tuple[str, ...],
22
+ factor: float,
23
+ ) -> float:
24
+ total = 0.0
25
+ for key, value in source.items():
26
+ if any(word in str(key).lower() for word in keywords):
27
+ total += float(value or 0.0)
28
+ return total * factor
29
+
30
+
31
+ def _count(source: dict[str, Any], key: str) -> float:
32
+ return float(source.get(key, 0.0) or 0.0)
33
+
34
+
35
+ def _excess_penalty(
36
+ count: float,
37
+ *,
38
+ grace: float,
39
+ factor: float,
40
+ cap: float,
41
+ ) -> float:
42
+ if count <= grace:
43
+ return 0.0
44
+ return min(cap, (count - grace) * factor)
45
+
46
+
47
+ def _file_modularity_penalty(by_metric: dict[str, Any]) -> float:
48
+ return (
49
+ _excess_penalty(_count(by_metric, 'module_lines'), grace=10.0, factor=1.4, cap=24.0)
50
+ + _excess_penalty(
51
+ _count(by_metric, 'class_public_methods'),
52
+ grace=4.0,
53
+ factor=1.2,
54
+ cap=8.0,
55
+ )
56
+ + _excess_penalty(
57
+ _count(by_metric, 'class_instance_attributes'),
58
+ grace=4.0,
59
+ factor=1.0,
60
+ cap=6.0,
61
+ )
62
+ )
63
+
64
+
65
+ def _maintainability_penalty(
66
+ by_metric: dict[str, Any],
67
+ by_severity: dict[str, Any],
68
+ ) -> float:
69
+ complexity = _excess_penalty(
70
+ _count(by_metric, 'cyclomatic_complexity'),
71
+ grace=32.0,
72
+ factor=0.45,
73
+ cap=18.0,
74
+ )
75
+ line_soft = _excess_penalty(
76
+ _count(by_metric, 'line_length_soft_hits'),
77
+ grace=55.0,
78
+ factor=0.06,
79
+ cap=6.0,
80
+ )
81
+ line_hard = _excess_penalty(
82
+ _count(by_metric, 'line_length_hard_hits'),
83
+ grace=16.0,
84
+ factor=0.22,
85
+ cap=8.0,
86
+ )
87
+ critical = _excess_penalty(
88
+ _count(by_severity, 'critical'),
89
+ grace=6.0,
90
+ factor=0.55,
91
+ cap=10.0,
92
+ )
93
+ return complexity + line_soft + line_hard + critical
94
+
95
+
96
+ def _topology_dimension(topology: dict[str, Any]) -> float:
97
+ if not isinstance(topology, dict) or not topology:
98
+ return 70.0
99
+
100
+ flat_file_total = int(topology.get('flat_file_total', 0) or 0)
101
+ subpackage_total = int(topology.get('subpackage_total', 0) or 0)
102
+ compat_wrapper_total = int(topology.get('compat_wrapper_total', 0) or 0)
103
+ placement_review = (
104
+ topology.get('root_placement_review', {})
105
+ if isinstance(topology.get('root_placement_review', {}), dict)
106
+ else {}
107
+ )
108
+ misplaced_root_total = len(placement_review.get('misplaced_root_files', []))
109
+ review_root_total = len(placement_review.get('review_root_files', []))
110
+ retained_root_total = len(placement_review.get('allowed_root_files', []))
111
+
112
+ score = 100.0
113
+ if flat_file_total > 8:
114
+ score -= min(34.0, (flat_file_total - 8) * 1.25)
115
+ if misplaced_root_total:
116
+ score -= min(26.0, misplaced_root_total * 1.15)
117
+ if review_root_total:
118
+ score -= min(12.0, review_root_total * 1.4)
119
+ if retained_root_total > 8:
120
+ score -= min(8.0, (retained_root_total - 8) * 1.6)
121
+
122
+ if subpackage_total >= 6:
123
+ score += 8.0
124
+ elif subpackage_total >= 3:
125
+ score += 5.0
126
+ elif subpackage_total > 0:
127
+ score += 2.5
128
+
129
+ if (
130
+ not bool(topology.get('needs_folder_management', False))
131
+ and flat_file_total <= 28
132
+ and misplaced_root_total <= 18
133
+ ):
134
+ score += 4.0
135
+ if compat_wrapper_total and misplaced_root_total == 0:
136
+ score += min(4.0, compat_wrapper_total * 0.8)
137
+
138
+ return round(clamp(score, 0.0, 100.0), 2)
139
+
140
+
141
+ def structure_dimensions(
142
+ history: dict[str, Any],
143
+ topology: dict[str, Any] | None = None,
144
+ *,
145
+ hotspot_digest: dict[str, Any] | None = None,
146
+ components: list[dict[str, Any]] | None = None,
147
+ cleanup: dict[str, Any] | None = None,
148
+ archive_candidates: dict[str, Any] | None = None,
149
+ semantic_judge: dict[str, Any] | None = None,
150
+ ) -> dict[str, float]:
151
+ summary = history.get('summary', {}) if isinstance(history.get('summary'), dict) else {}
152
+ by_metric = summary.get('by_metric', {}) if isinstance(summary.get('by_metric'), dict) else {}
153
+ by_dimension = (
154
+ summary.get('by_dimension', {})
155
+ if isinstance(summary.get('by_dimension'), dict)
156
+ else {}
157
+ )
158
+ by_severity = (
159
+ summary.get('by_severity', {})
160
+ if isinstance(summary.get('by_severity'), dict)
161
+ else {}
162
+ )
163
+
164
+ file_modularity = 100.0 - min(36.0, _file_modularity_penalty(by_metric))
165
+ boundary_clarity = 100.0 - min(
166
+ 40.0,
167
+ score_from_keywords(by_dimension, ('boundary', 'layer', 'ownership', 'component'), 2.0),
168
+ )
169
+ coupling = 100.0 - min(
170
+ 35.0,
171
+ score_from_keywords(by_dimension, ('coupling', 'dependency'), 2.4),
172
+ )
173
+ maintainability = 100.0 - min(
174
+ 36.0,
175
+ _maintainability_penalty(by_metric, by_severity),
176
+ )
177
+ dimensions = {
178
+ 'file_modularity': round(max(0.0, file_modularity), 2),
179
+ 'boundary_clarity': round(max(0.0, boundary_clarity), 2),
180
+ 'coupling_control': round(max(0.0, coupling), 2),
181
+ 'maintainability': round(max(0.0, maintainability), 2),
182
+ }
183
+ if topology is not None:
184
+ dimensions['package_topology'] = _topology_dimension(topology)
185
+ dimensions.update(
186
+ governance_dimensions(
187
+ hotspot_digest=hotspot_digest,
188
+ components=components,
189
+ cleanup=cleanup,
190
+ archive_candidates=archive_candidates,
191
+ semantic_judge=semantic_judge,
192
+ )
193
+ )
194
+ return dimensions
195
+
196
+
197
+ def structure_score(full_score: dict[str, Any], dimensions: dict[str, float]) -> float:
198
+ base = float(full_score.get('score', 0.0) or 0.0)
199
+ if not dimensions:
200
+ return round(base, 2)
201
+ avg = sum(float(v or 0.0) for v in dimensions.values()) / max(1, len(dimensions))
202
+ return round((base * 0.3) + (avg * 0.7), 2)
203
+
204
+
205
+ def incremental_score(score: dict[str, Any], *, diff: bool) -> dict[str, Any]:
206
+ if diff and isinstance(score.get('incremental_score'), dict):
207
+ return score['incremental_score']
208
+ return {'mode': 'not_applicable', 'score': 0.0, 'recommendation': 'n/a', 'signals': {}}
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from architec.analysis.analysis_cache import run_cached_analysis
7
+ from architec.backend_llm import complete_json
8
+ from architec.support.llm_guard import guard_llm_result
9
+
10
+
11
+ def llm_summary(root: Path, *, payload: dict[str, Any]) -> dict[str, Any] | None:
12
+ prompt = f"Input:\n{payload}"
13
+ result, cache_hit = run_cached_analysis(
14
+ root,
15
+ namespace="architec_summary",
16
+ payload=payload,
17
+ runner=lambda: guard_llm_result(
18
+ root,
19
+ task="architec_summary",
20
+ runner=lambda: complete_json(
21
+ root,
22
+ task="architec_summary",
23
+ tier="strong",
24
+ prompt=prompt,
25
+ timeout_sec=30.0,
26
+ max_tokens=900,
27
+ required=True,
28
+ ),
29
+ ),
30
+ )
31
+ if not isinstance(result, dict):
32
+ return None
33
+ out = dict(result)
34
+ out["_cache_hit"] = bool(cache_hit)
35
+ return out
36
+
37
+
38
+ def run_diff_analysis(root: Path, *, diff: bool, base: str, head: str, runner) -> dict[str, Any]:
39
+ if not diff:
40
+ return {}
41
+ return runner(root, base=base or None, head=head or None, llm_enabled=True)
42
+
43
+
44
+ def run_goal_analysis(root: Path, *, goal: str, runner) -> dict[str, Any]:
45
+ if not goal:
46
+ return {}
47
+ return runner(root, goal=goal, llm_enabled=True)