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.
- architec/__init__.py +52 -0
- architec/__main__.py +4 -0
- architec/_compat_reexport.py +20 -0
- architec/_version.py +4 -0
- architec/advice_feedback.py +263 -0
- architec/analysis/__init__.py +5 -0
- architec/analysis/analysis_cache.py +76 -0
- architec/analysis/analysis_runner_flow.py +208 -0
- architec/analysis/analysis_runner_llm.py +47 -0
- architec/analysis/analysis_runner_recommendations.py +104 -0
- architec/analysis/analysis_runner_report.py +174 -0
- architec/analysis/analysis_runner_report_support.py +104 -0
- architec/analysis/analysis_runner_report_views.py +188 -0
- architec/analysis/governance_dimensions.py +195 -0
- architec/analysis/history_analyzer.py +198 -0
- architec/analysis/history_analyzer_report.py +96 -0
- architec/analysis/hotspot_digest.py +62 -0
- architec/analysis/hotspot_digest_rank.py +150 -0
- architec/analysis/hotspot_digest_sources.py +156 -0
- architec/analysis/public.py +234 -0
- architec/analysis/repo_topology.py +235 -0
- architec/analysis/repo_topology_findings.py +151 -0
- architec/analysis/repo_topology_group_terms.py +199 -0
- architec/analysis/repo_topology_groups.py +168 -0
- architec/analysis/repo_topology_llm.py +192 -0
- architec/analysis/repo_topology_migration.py +266 -0
- architec/analysis/repo_topology_migration_helpers.py +247 -0
- architec/analysis/repo_topology_paths.py +123 -0
- architec/analysis/repo_topology_review_helpers.py +204 -0
- architec/analysis/repo_topology_rule_data.py +121 -0
- architec/analysis/repo_topology_rules.py +119 -0
- architec/analysis_runner.py +3 -0
- architec/auth/__init__.py +21 -0
- architec/auth/client.py +149 -0
- architec/auth/commands.py +658 -0
- architec/auth/device.py +36 -0
- architec/auth/guard.py +65 -0
- architec/auth/lease.py +77 -0
- architec/auth/store.py +78 -0
- architec/backend_llm/__init__.py +177 -0
- architec/backend_llm/cache.py +143 -0
- architec/backend_llm/config.py +370 -0
- architec/backend_llm/errors.py +20 -0
- architec/backend_llm/failover.py +158 -0
- architec/backend_llm/flow.py +401 -0
- architec/backend_llm/gateway.py +160 -0
- architec/backend_llm/parse.py +144 -0
- architec/baseline/__init__.py +11 -0
- architec/baseline/report.py +227 -0
- architec/cleanup/__init__.py +67 -0
- architec/cleanup/archive.py +205 -0
- architec/cleanup/autofix.py +258 -0
- architec/cleanup/inventory.py +229 -0
- architec/cleanup/metadata.py +46 -0
- architec/cleanup/report.py +119 -0
- architec/cleanup/retire_plan.py +380 -0
- architec/cleanup/scope.py +134 -0
- architec/cleanup/semantic_judge.py +451 -0
- architec/cli.py +973 -0
- architec/code_review/__init__.py +9 -0
- architec/code_review/architecture_contracts.py +190 -0
- architec/code_review/near_duplicate.py +700 -0
- architec/code_review/plan_diff_consistency.py +1201 -0
- architec/code_review/public.py +2156 -0
- architec/code_review/python_imports.py +96 -0
- architec/code_review/risk_context.py +376 -0
- architec/code_review/scan_cache.py +112 -0
- architec/code_review/shadow_implementation.py +1261 -0
- architec/component_descriptors.py +3 -0
- architec/descriptors/__init__.py +17 -0
- architec/descriptors/component_descriptors_builder.py +104 -0
- architec/descriptors/component_descriptors_semantics.py +19 -0
- architec/descriptors/component_descriptors_semantics_roles.py +75 -0
- architec/descriptors/component_descriptors_semantics_terms.py +95 -0
- architec/descriptors/component_descriptors_symbols.py +82 -0
- architec/descriptors/component_graph.py +108 -0
- architec/descriptors/public.py +15 -0
- architec/events/__init__.py +3 -0
- architec/events/public.py +132 -0
- architec/feature/__init__.py +5 -0
- architec/feature/feature_advisor.py +130 -0
- architec/feature/feature_advisor_llm.py +55 -0
- architec/feature/feature_advisor_ranking.py +88 -0
- architec/feature/feature_advisor_ranking_output.py +55 -0
- architec/feature/feature_advisor_ranking_phase1.py +95 -0
- architec/feature/feature_advisor_ranking_phase2.py +91 -0
- architec/feature/feature_advisor_targets.py +159 -0
- architec/feature/feature_query.py +128 -0
- architec/feature/feature_query_scoring.py +120 -0
- architec/fix_advice/__init__.py +3 -0
- architec/fix_advice/public.py +518 -0
- architec/gate/__init__.py +13 -0
- architec/gate/report.py +265 -0
- architec/integration/__init__.py +1 -0
- architec/integration/bundle_loader.py +287 -0
- architec/integration/hippo_adapter.py +181 -0
- architec/integration/hippo_adapter_paths.py +177 -0
- architec/integration/hippo_adapter_snapshot.py +27 -0
- architec/integration/hippo_bridge.py +101 -0
- architec/integration/paths.py +35 -0
- architec/integration/resource_paths.py +131 -0
- architec/orchestrator/__init__.py +194 -0
- architec/orchestrator/component_qa.py +189 -0
- architec/orchestrator/component_qa_selection.py +180 -0
- architec/orchestrator/orchestrator_batch_builders.py +168 -0
- architec/orchestrator/orchestrator_batch_helpers.py +135 -0
- architec/orchestrator/orchestrator_batches.py +119 -0
- architec/orchestrator/orchestrator_flow.py +152 -0
- architec/orchestrator/orchestrator_llm.py +39 -0
- architec/orchestrator/orchestrator_report.py +105 -0
- architec/orchestrator/orchestrator_test_plan.py +495 -0
- architec/orchestrator/orchestrator_timing.py +11 -0
- architec/plan_review/__init__.py +7 -0
- architec/plan_review/public.py +160 -0
- architec/project_status/__init__.py +3 -0
- architec/project_status/public.py +117 -0
- architec/reporting/__init__.py +1 -0
- architec/reporting/architecture_report_compaction.py +172 -0
- architec/reporting/architecture_report_md.py +160 -0
- architec/reporting/architecture_report_sections.py +181 -0
- architec/reporting/report_markdown.py +360 -0
- architec/reporting/viz_generator.py +157 -0
- architec/reporting/viz_generator_cards.py +59 -0
- architec/reporting/viz_generator_sections.py +43 -0
- architec/reporting/viz_generator_view.py +288 -0
- architec/scoring/__init__.py +19 -0
- architec/scoring/component_scoring.py +136 -0
- architec/scoring/component_scoring_git.py +75 -0
- architec/scoring/component_scoring_llm.py +35 -0
- architec/scoring/component_scoring_payload.py +44 -0
- architec/scoring/component_scoring_registry.py +93 -0
- architec/scoring/component_scoring_runtime.py +182 -0
- architec/scoring/component_scoring_scope.py +50 -0
- architec/scoring/component_selection_policy.py +58 -0
- architec/scoring/contract_engine.py +142 -0
- architec/scoring/public.py +50 -0
- architec/scoring/scoring_policy_common.py +68 -0
- architec/scoring/scoring_policy_defaults.py +76 -0
- architec/scoring/scoring_policy_full_eval.py +208 -0
- architec/scoring/scoring_policy_incremental_eval.py +103 -0
- architec/scoring/scoring_policy_incremental_helpers.py +79 -0
- architec/scoring/scoring_policy_incremental_reasoning.py +73 -0
- architec/scoring/scoring_policy_overall_eval.py +146 -0
- architec/scoring_policy.py +3 -0
- architec/self_manage.py +296 -0
- architec/support/__init__.py +1 -0
- architec/support/architecture_rules.py +376 -0
- architec/support/io_utils.py +45 -0
- architec/support/llm_guard.py +88 -0
- architec/support/llm_preflight.py +128 -0
- architec/support/path_policy.py +326 -0
- architec/support/refresh_decider.py +241 -0
- architec/support/refresh_decider_git.py +105 -0
- architec/support/tls.py +42 -0
- architec/version.py +44 -0
- architec-0.2.11.data/data/architec/config/config.default.yaml +38 -0
- architec-0.2.11.data/data/architec/config/config.example.yaml +38 -0
- architec-0.2.11.data/data/architec/config/rubric.json +76 -0
- architec-0.2.11.data/data/architec/config/scoring-policy.json +86 -0
- architec-0.2.11.data/data/architec/prompts/analyze.md +82 -0
- architec-0.2.11.data/data/architec/prompts/codex-repair.md +36 -0
- architec-0.2.11.data/data/architec/prompts/component-qa.md +10 -0
- architec-0.2.11.data/data/architec/prompts/component-scoring.md +17 -0
- architec-0.2.11.data/data/architec/prompts/feature-architecture.md +13 -0
- architec-0.2.11.data/data/architec/prompts/folder-naming-judge.md +129 -0
- architec-0.2.11.data/data/architec/prompts/full-report.md +14 -0
- architec-0.2.11.data/data/architec/prompts/history-remediation.md +13 -0
- architec-0.2.11.data/data/architec/prompts/orchestrator-program.md +14 -0
- architec-0.2.11.data/data/architec/prompts/review-diff.md +40 -0
- architec-0.2.11.data/data/architec/prompts/semantic-judge.md +69 -0
- architec-0.2.11.data/data/architec/prompts/split-expert.md +30 -0
- architec-0.2.11.data/data/architec/prompts/summary.md +14 -0
- architec-0.2.11.data/data/architec/prompts/system.md +60 -0
- architec-0.2.11.data/data/architec/prompts/tool-orchestrator.md +65 -0
- architec-0.2.11.data/data/architec/prompts/topology-review-judge.md +106 -0
- architec-0.2.11.data/data/architec/tools/archi_entry.py +12 -0
- architec-0.2.11.data/data/architec/tools/build_architect_prompt.py +223 -0
- architec-0.2.11.data/data/architec/tools/collect_repo_metrics.py +282 -0
- architec-0.2.11.data/data/architec/tools/collect_repo_metrics_python.py +187 -0
- architec-0.2.11.data/data/architec/tools/collect_repo_metrics_rules.py +26 -0
- architec-0.2.11.data/data/architec/tools/collect_repo_metrics_runtime.py +92 -0
- architec-0.2.11.data/data/architec/tools/collect_repo_metrics_scan.py +141 -0
- architec-0.2.11.data/data/architec/tools/prod_browser_auth_smoke.sh +390 -0
- architec-0.2.11.data/data/architec/tools/refresh_architect_context.sh +16 -0
- architec-0.2.11.data/data/architec/tools/run_archi_via_auth_tunnel.sh +141 -0
- architec-0.2.11.dist-info/METADATA +322 -0
- architec-0.2.11.dist-info/RECORD +191 -0
- architec-0.2.11.dist-info/WHEEL +5 -0
- architec-0.2.11.dist-info/entry_points.txt +2 -0
- architec-0.2.11.dist-info/licenses/LICENSE +21 -0
- 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,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,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,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)
|