codeabc 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- backend/__init__.py +0 -0
- backend/app.py +95 -0
- backend/desktop_server.py +46 -0
- backend/models.py +629 -0
- backend/prompts/__init__.py +0 -0
- backend/prompts/annotate.py +44 -0
- backend/prompts/edit.py +62 -0
- backend/prompts/overview.py +57 -0
- backend/prompts/qa.py +45 -0
- backend/routers/__init__.py +0 -0
- backend/routers/analyze.py +271 -0
- backend/routers/project.py +872 -0
- backend/services/__init__.py +0 -0
- backend/services/action_plan.py +366 -0
- backend/services/activity.py +313 -0
- backend/services/apimap.py +391 -0
- backend/services/async_surface.py +123 -0
- backend/services/cache.py +183 -0
- backend/services/churn.py +244 -0
- backend/services/ci_checks.py +317 -0
- backend/services/codemap_export.py +136 -0
- backend/services/commands.py +276 -0
- backend/services/complexity.py +111 -0
- backend/services/config_files.py +190 -0
- backend/services/contributing.py +308 -0
- backend/services/coverage.py +283 -0
- backend/services/datamodels.py +190 -0
- backend/services/deep_nesting.py +110 -0
- backend/services/dependencies.py +270 -0
- backend/services/docs.py +255 -0
- backend/services/docstrings.py +163 -0
- backend/services/duplicate_code.py +165 -0
- backend/services/entrypoints.py +152 -0
- backend/services/envscan.py +167 -0
- backend/services/error_handling.py +239 -0
- backend/services/filenames.py +261 -0
- backend/services/github_clone.py +129 -0
- backend/services/glossary.py +96 -0
- backend/services/health_score.py +381 -0
- backend/services/importgraph.py +986 -0
- backend/services/integrations.py +263 -0
- backend/services/licenses.py +505 -0
- backend/services/llm.py +117 -0
- backend/services/long_functions.py +101 -0
- backend/services/ownership.py +194 -0
- backend/services/package_purposes.py +112 -0
- backend/services/pr_reading.py +181 -0
- backend/services/release_map.py +402 -0
- backend/services/report_export.py +232 -0
- backend/services/risk.py +88 -0
- backend/services/scanner.py +546 -0
- backend/services/schedules.py +369 -0
- backend/services/security.py +445 -0
- backend/services/settings_map.py +234 -0
- backend/services/symbols.py +605 -0
- backend/services/techdebt.py +124 -0
- backend/services/too_many_params.py +102 -0
- backend/services/typing_coverage.py +184 -0
- codeabc-0.1.0.dist-info/METADATA +283 -0
- codeabc-0.1.0.dist-info/RECORD +62 -0
- codeabc-0.1.0.dist-info/WHEEL +4 -0
- codeabc-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Terminology dictionary: plain-language definitions for jargon in code.
|
|
2
|
+
|
|
3
|
+
A reader new to a codebase trips over vocabulary as much as logic. This is a
|
|
4
|
+
curated dictionary of common programming terms with everyday, jargon-free
|
|
5
|
+
explanations (the same voice as the line annotations), plus a scanner that
|
|
6
|
+
reports which of those terms actually appear in a given file so the UI can
|
|
7
|
+
offer "hover a keyword, see what it means".
|
|
8
|
+
|
|
9
|
+
Deterministic — no LLM call — so it's instant, free, and unit-testable.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
# term (lowercase canonical) -> plain-language explanation.
|
|
17
|
+
# Definitions deliberately avoid jargon and lean on everyday analogies.
|
|
18
|
+
GLOSSARY: dict[str, str] = {
|
|
19
|
+
"api": "一套约定好的「对外接口」,别的程序按这套约定就能调用你的功能,像餐厅菜单:照着点就行,不用进厨房。",
|
|
20
|
+
"async": "异步:发起一件耗时的事后不干等着,先去做别的,等它好了再回来处理,像点了外卖继续干活而不是站在门口等。",
|
|
21
|
+
"await": "等一件异步的事真正完成再往下走,相当于「这一步必须拿到结果才能继续」。",
|
|
22
|
+
"cache": "缓存:把算过/取过的结果先存起来,下次直接拿,省得重复跑,像把常用调料放灶台边而不是每次去仓库。",
|
|
23
|
+
"callback": "回调:把一个函数交给别人,让对方在某件事发生时替你调用它,像留个电话「好了打给我」。",
|
|
24
|
+
"closure": "闭包:一个函数把它出生时周围的变量一起「打包带走」,之后还能用到那些变量,像便当带上了家里的菜。",
|
|
25
|
+
"concurrency": "并发:让多件事在时间上交错推进,看起来同时在做,像一个人左右手轮流照看几口锅。",
|
|
26
|
+
"coroutine": "协程:能中途暂停、之后从原地接着跑的函数,配合异步用来高效处理大量等待。",
|
|
27
|
+
"decorator": "装饰器:在不改原函数的前提下给它套一层额外行为,像给手机套个壳,功能多了但手机没变。",
|
|
28
|
+
"dependency": "依赖:这段代码要正常工作所必须用到的别的库或模块,缺了就跑不起来。",
|
|
29
|
+
"deserialize": "反序列化:把存成文本/字节的数据还原成程序里能用的对象,是序列化的逆操作。",
|
|
30
|
+
"endpoint": "端点:服务对外暴露的一个具体网址入口,访问它就能触发某个功能。",
|
|
31
|
+
"enum": "枚举:把一组固定的可选值起好名字管起来,比如「红/黄/绿」,避免到处写魔法数字。",
|
|
32
|
+
"generator": "生成器:需要时才一个一个「现产」数据而不是一次全做好,省内存,像点一份做一份。",
|
|
33
|
+
"hash": "哈希:把任意数据压成一个固定长度的「指纹」,常用来快速比对或定位。",
|
|
34
|
+
"idempotent": "幂等:同样的操作做一次和做多次效果一样,像电梯按钮多按几下也只来一趟。",
|
|
35
|
+
"inheritance": "继承:新类直接沿用并扩展已有类的能力,像孩子继承父母的部分特征再加自己的。",
|
|
36
|
+
"iterator": "迭代器:负责「逐个往下取」集合里元素的东西,for 循环背后就靠它。",
|
|
37
|
+
"lambda": "匿名函数:临时用、不取名字的小函数,写在用它的地方,用完即弃。",
|
|
38
|
+
"lazy": "惰性:能拖到真正需要时再算/再加载,不提前浪费力气。",
|
|
39
|
+
"lock": "锁:同一时刻只让一个人动共享数据,防止大家一起改乱套,像洗手间的门闩。",
|
|
40
|
+
"memoize": "记忆化:把函数对某组输入算出的结果缓存下来,下次同样输入直接返回。",
|
|
41
|
+
"middleware": "中间件:夹在请求和真正处理之间的一层,统一做鉴权、日志等杂活,像进场前的安检。",
|
|
42
|
+
"mutex": "互斥锁:保证同一份资源同一时间只被一个线程使用的开关。",
|
|
43
|
+
"orm": "对象关系映射:让你用操作对象的方式读写数据库,不必手写 SQL,像翻译官帮你和数据库对话。",
|
|
44
|
+
"polymorphism": "多态:同一个调用,不同对象给出各自的实现,像「叫一声」猫和狗反应不同。",
|
|
45
|
+
"promise": "承诺对象:代表一件「将来才会有结果」的事,先拿到凭据,结果到了再兑现。",
|
|
46
|
+
"recursion": "递归:函数自己调用自己来把大问题拆成同样的小问题,像两面镜子互照。",
|
|
47
|
+
"refactor": "重构:在不改变外部行为的前提下整理代码结构,让它更清晰好维护。",
|
|
48
|
+
"regex": "正则表达式:用一串符号描述「什么样的文本算匹配」,用来查找或校验字符串,像高级版的查找替换。",
|
|
49
|
+
"serialize": "序列化:把程序里的对象转成可保存或可传输的文本/字节,方便存盘或发送。",
|
|
50
|
+
"singleton": "单例:整个程序里某个东西只允许存在一个实例,大家共用它。",
|
|
51
|
+
"stub": "桩:测试时用来顶替真实依赖的简化假货,让测试不碰外部系统。",
|
|
52
|
+
"thread": "线程:程序里一条独立执行的「流水线」,多条线程可以并行干活。",
|
|
53
|
+
"throttle": "节流:限制某操作的触发频率,太频繁就先压一压,避免把系统压垮。",
|
|
54
|
+
"token": "令牌:一小段代表身份或权限的凭证,带着它就能证明「我是谁、能干啥」。",
|
|
55
|
+
"tuple": "元组:一串按顺序排好、且不可修改的值,像一张定好的座位表。",
|
|
56
|
+
"yield": "把当前结果先「吐」出去并暂停,等下次再要时从这里继续,是生成器的核心动作。",
|
|
57
|
+
"race condition": "竞态:多个执行流同时改一份数据、谁先谁后没定准,导致结果飘忽的 bug。",
|
|
58
|
+
"dependency injection": "依赖注入:一个组件需要的零件由外部传进来,而不是自己 new,方便替换和测试。",
|
|
59
|
+
"garbage collection": "垃圾回收:运行时自动把不再用到的内存清掉,省得你手动释放。",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Pre-build one matcher per term so we can map matches back to the canonical
|
|
63
|
+
# key. Phrases allow flexible whitespace; all matching is whole-word and
|
|
64
|
+
# case-insensitive so "in" never lights up inside "string".
|
|
65
|
+
_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
|
|
66
|
+
(
|
|
67
|
+
term,
|
|
68
|
+
re.compile(
|
|
69
|
+
r"\b" + r"\s+".join(re.escape(part) for part in term.split()) + r"\b",
|
|
70
|
+
re.IGNORECASE,
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
for term in GLOSSARY
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def lookup(term: str) -> str | None:
|
|
78
|
+
"""Return the definition for *term* (case-insensitive), or ``None``."""
|
|
79
|
+
return GLOSSARY.get(term.strip().lower())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def scan_terms(text: str) -> list[dict]:
|
|
83
|
+
"""Return the glossary terms that appear in *text*, with definitions.
|
|
84
|
+
|
|
85
|
+
Each entry is ``{"term", "definition"}``. Results are de-duplicated and
|
|
86
|
+
sorted alphabetically so the panel order is stable.
|
|
87
|
+
"""
|
|
88
|
+
if not text:
|
|
89
|
+
return []
|
|
90
|
+
found = [
|
|
91
|
+
{"term": term, "definition": GLOSSARY[term]}
|
|
92
|
+
for term, pattern in _PATTERNS
|
|
93
|
+
if pattern.search(text)
|
|
94
|
+
]
|
|
95
|
+
found.sort(key=lambda e: e["term"])
|
|
96
|
+
return found
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""Project health score — aggregate signal from multiple analyses into a single 0-100 rating.
|
|
2
|
+
|
|
3
|
+
When a non-programmer receives a code review, a vendor codebase, or an inherited project,
|
|
4
|
+
they need a fast answer to the question: "is this code in good shape?" Before this module,
|
|
5
|
+
CodeABC produced several independent analyses (security, test coverage, activity, complexity,
|
|
6
|
+
tech debt, architecture) but left it to the viewer to mentally weigh them.
|
|
7
|
+
|
|
8
|
+
This module synthesises those signals into one score and grade.
|
|
9
|
+
|
|
10
|
+
Scoring dimensions and weights:
|
|
11
|
+
Security 25 % hardcoded secrets / dangerous calls cost heavily
|
|
12
|
+
Test coverage 20 % ratio of source files covered by tests
|
|
13
|
+
Activity 20 % recent commit cadence from the activity pulse
|
|
14
|
+
Complexity 15 % average cyclomatic complexity of source files
|
|
15
|
+
Tech debt 10 % density of TODO/FIXME/HACK markers
|
|
16
|
+
Architecture 10 % import-cycle count; orphan modules contribute to noise
|
|
17
|
+
|
|
18
|
+
Grade mapping:
|
|
19
|
+
A 90–100 excellent
|
|
20
|
+
B 75–89 good
|
|
21
|
+
C 60–74 fair — consider targeted improvements
|
|
22
|
+
D 40–59 needs work
|
|
23
|
+
F 0–39 critical issues present
|
|
24
|
+
|
|
25
|
+
Pure function over already-computed analysis dicts — no repo access required.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Internal helpers
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
_ACTIVITY_LABEL_SCORE: dict[str, int] = {
|
|
35
|
+
"active": 100,
|
|
36
|
+
"slowing": 72,
|
|
37
|
+
"quiet": 48,
|
|
38
|
+
"stale": 22,
|
|
39
|
+
"abandoned": 0,
|
|
40
|
+
"unknown": 50,
|
|
41
|
+
"": 50,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_GRADE_THRESHOLDS: list[tuple[int, str]] = [
|
|
45
|
+
(90, "A"),
|
|
46
|
+
(75, "B"),
|
|
47
|
+
(60, "C"),
|
|
48
|
+
(40, "D"),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _grade(score: int) -> str:
|
|
53
|
+
for threshold, letter in _GRADE_THRESHOLDS:
|
|
54
|
+
if score >= threshold:
|
|
55
|
+
return letter
|
|
56
|
+
return "F"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _security_score(security: dict | None) -> int:
|
|
60
|
+
"""100 = no issues; subtract for critical and non-critical findings."""
|
|
61
|
+
if not security or not security.get("total"):
|
|
62
|
+
return 100
|
|
63
|
+
critical = security.get("critical", 0)
|
|
64
|
+
total = security.get("total", 0)
|
|
65
|
+
non_critical = total - critical
|
|
66
|
+
s = 100 - critical * 25 - non_critical * 8
|
|
67
|
+
return max(0, s)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _coverage_score(test_coverage: dict | None) -> int:
|
|
71
|
+
if not test_coverage:
|
|
72
|
+
return 50 # unknown: neutral
|
|
73
|
+
pct = test_coverage.get("coverage_percent", 0)
|
|
74
|
+
# Reward coverage non-linearly: 0%→0, 50%→55, 80%→80, 100%→100
|
|
75
|
+
if pct >= 80:
|
|
76
|
+
return pct
|
|
77
|
+
if pct >= 50:
|
|
78
|
+
# scale 50-80 → 55-80
|
|
79
|
+
return int(55 + (pct - 50) * (80 - 55) / (80 - 50))
|
|
80
|
+
if pct > 0:
|
|
81
|
+
# scale 1-49 → 5-54
|
|
82
|
+
return max(5, int(pct * 54 / 49))
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _activity_score(activity: dict | None) -> int:
|
|
87
|
+
if not activity or not activity.get("available", True):
|
|
88
|
+
# uploaded project without git — neutral
|
|
89
|
+
return 50
|
|
90
|
+
label = activity.get("label", "unknown")
|
|
91
|
+
return _ACTIVITY_LABEL_SCORE.get(label, 50)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _complexity_score(
|
|
95
|
+
complexity_files: list[dict] | None,
|
|
96
|
+
total_files: int,
|
|
97
|
+
deep_nesting_files: list[dict] | None = None,
|
|
98
|
+
) -> int:
|
|
99
|
+
"""Higher complexity → lower score.
|
|
100
|
+
|
|
101
|
+
Complexity files are already the most complex ones in the project. We
|
|
102
|
+
penalise for having highly complex files relative to project size, using two
|
|
103
|
+
distinct signals: cyclomatic complexity (how many branches) and nesting depth
|
|
104
|
+
(how deeply those branches are stacked) — a function can be hard to follow on
|
|
105
|
+
either axis independently.
|
|
106
|
+
"""
|
|
107
|
+
if (not complexity_files and not deep_nesting_files) or total_files == 0:
|
|
108
|
+
return 100
|
|
109
|
+
# Complexity threshold: cyclomatic complexity > 20 is considered "high"
|
|
110
|
+
HIGH_THRESHOLD = 20
|
|
111
|
+
VERY_HIGH_THRESHOLD = 50
|
|
112
|
+
high_count = sum(1 for f in (complexity_files or []) if f.get("complexity", 0) > HIGH_THRESHOLD)
|
|
113
|
+
very_high_count = sum(
|
|
114
|
+
1 for f in (complexity_files or []) if f.get("complexity", 0) > VERY_HIGH_THRESHOLD
|
|
115
|
+
)
|
|
116
|
+
# Fraction of project that is "high complexity"
|
|
117
|
+
high_frac = high_count / max(total_files, 1)
|
|
118
|
+
very_high_frac = very_high_count / max(total_files, 1)
|
|
119
|
+
s = 100 - int(high_frac * 60) - int(very_high_frac * 20)
|
|
120
|
+
# Deep nesting is a separate readability cost from branch count: penalise
|
|
121
|
+
# functions nested very deep (>= 6 levels) relative to project size.
|
|
122
|
+
if deep_nesting_files:
|
|
123
|
+
deep_count = sum(1 for f in deep_nesting_files if f.get("depth", 0) >= 6)
|
|
124
|
+
s -= int((deep_count / max(total_files, 1)) * 30)
|
|
125
|
+
return max(0, s)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _tech_debt_score(tech_debt_files: list[dict] | None, total_files: int) -> int:
|
|
129
|
+
"""Score based on density of TODO/FIXME/HACK markers across the project."""
|
|
130
|
+
if not tech_debt_files or total_files == 0:
|
|
131
|
+
return 100
|
|
132
|
+
total_markers = sum(f.get("count", 0) for f in tech_debt_files)
|
|
133
|
+
markers_per_file = total_markers / max(total_files, 1)
|
|
134
|
+
# 0 markers/file → 100; 1/file → ~80; 3/file → ~55; 10+/file → ~20
|
|
135
|
+
if markers_per_file == 0:
|
|
136
|
+
return 100
|
|
137
|
+
if markers_per_file < 0.5:
|
|
138
|
+
return 90
|
|
139
|
+
if markers_per_file < 1:
|
|
140
|
+
return 80
|
|
141
|
+
if markers_per_file < 2:
|
|
142
|
+
return 68
|
|
143
|
+
if markers_per_file < 5:
|
|
144
|
+
return int(68 - (markers_per_file - 2) * 9)
|
|
145
|
+
return max(15, int(50 - markers_per_file * 3))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _architecture_score(
|
|
149
|
+
import_cycles: list[dict] | None,
|
|
150
|
+
orphan_modules: list[dict] | None,
|
|
151
|
+
total_files: int,
|
|
152
|
+
) -> int:
|
|
153
|
+
"""Score based on import cycles and orphan modules."""
|
|
154
|
+
cycles = len(import_cycles) if import_cycles else 0
|
|
155
|
+
orphans = len(orphan_modules) if orphan_modules else 0
|
|
156
|
+
if cycles == 0 and orphans == 0:
|
|
157
|
+
return 100
|
|
158
|
+
orphan_frac = orphans / max(total_files, 1)
|
|
159
|
+
s = 100 - min(cycles * 20, 60) - int(min(orphan_frac * 30, 30))
|
|
160
|
+
return max(0, s)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# Strengths / weaknesses narrative
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _build_strengths_weaknesses(
|
|
169
|
+
cat_scores: dict[str, int],
|
|
170
|
+
security: dict | None,
|
|
171
|
+
test_coverage: dict | None,
|
|
172
|
+
complexity_files: list[dict] | None,
|
|
173
|
+
tech_debt_files: list[dict] | None,
|
|
174
|
+
import_cycles: list[dict] | None,
|
|
175
|
+
) -> tuple[list[str], list[str]]:
|
|
176
|
+
strengths: list[str] = []
|
|
177
|
+
weaknesses: list[str] = []
|
|
178
|
+
|
|
179
|
+
# Security
|
|
180
|
+
if cat_scores["security"] == 100:
|
|
181
|
+
strengths.append("未发现硬编码密钥或高危调用,安全基线良好。")
|
|
182
|
+
elif cat_scores["security"] < 60:
|
|
183
|
+
crit = (security or {}).get("critical", 0)
|
|
184
|
+
weaknesses.append(f"发现 {crit} 处高危安全问题(硬编码密钥或危险调用),应优先处理。")
|
|
185
|
+
elif cat_scores["security"] < 80:
|
|
186
|
+
weaknesses.append("存在轻度安全隐患,建议排查 security 报告中的 findings。")
|
|
187
|
+
|
|
188
|
+
# Test coverage
|
|
189
|
+
pct = (test_coverage or {}).get("coverage_percent", -1)
|
|
190
|
+
if pct >= 75:
|
|
191
|
+
strengths.append(f"测试覆盖率 {pct}%,源文件覆盖充分。")
|
|
192
|
+
elif pct >= 0:
|
|
193
|
+
weaknesses.append(f"测试覆盖率仅 {pct}%,核心文件建议补充单元测试。")
|
|
194
|
+
else:
|
|
195
|
+
weaknesses.append("缺少测试文件,无法评估测试覆盖率。")
|
|
196
|
+
|
|
197
|
+
# Activity
|
|
198
|
+
if cat_scores["activity"] >= 90:
|
|
199
|
+
strengths.append("项目处于活跃维护期,近期提交频繁。")
|
|
200
|
+
elif cat_scores["activity"] == 0:
|
|
201
|
+
weaknesses.append("项目超过一年无新提交,疑似停摆,使用前请评估维护风险。")
|
|
202
|
+
elif cat_scores["activity"] < 50:
|
|
203
|
+
weaknesses.append("项目活跃度较低,近 3 个月内提交稀少。")
|
|
204
|
+
|
|
205
|
+
# Complexity
|
|
206
|
+
if cat_scores["complexity"] >= 90:
|
|
207
|
+
strengths.append("代码复杂度在可控范围内,没有明显的「上帝函数」。")
|
|
208
|
+
elif cat_scores["complexity"] < 60:
|
|
209
|
+
high = [f for f in (complexity_files or []) if f.get("complexity", 0) > 20]
|
|
210
|
+
weaknesses.append(f"{len(high)} 个文件复杂度偏高(圈复杂度 >20),阅读和维护成本大。")
|
|
211
|
+
|
|
212
|
+
# Tech debt
|
|
213
|
+
if cat_scores["tech_debt"] >= 90:
|
|
214
|
+
strengths.append("技术债标记(TODO/FIXME/HACK)极少,代码整洁。")
|
|
215
|
+
elif cat_scores["tech_debt"] < 55:
|
|
216
|
+
total_m = sum(f.get("count", 0) for f in (tech_debt_files or []))
|
|
217
|
+
weaknesses.append(f"技术债标记共 {total_m} 处,建议择期清理。")
|
|
218
|
+
|
|
219
|
+
# Architecture
|
|
220
|
+
if cat_scores["architecture"] >= 90:
|
|
221
|
+
strengths.append("无循环依赖,模块边界清晰。")
|
|
222
|
+
elif import_cycles:
|
|
223
|
+
weaknesses.append(f"存在 {len(import_cycles)} 个循环依赖组,可能导致难以理解的耦合。")
|
|
224
|
+
|
|
225
|
+
return strengths, weaknesses
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# Public API
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
_WEIGHTS: dict[str, float] = {
|
|
233
|
+
"security": 0.25,
|
|
234
|
+
"test_coverage": 0.20,
|
|
235
|
+
"activity": 0.20,
|
|
236
|
+
"complexity": 0.15,
|
|
237
|
+
"tech_debt": 0.10,
|
|
238
|
+
"architecture": 0.10,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def compute_health_score(
|
|
243
|
+
*,
|
|
244
|
+
security: dict | None = None,
|
|
245
|
+
test_coverage: dict | None = None,
|
|
246
|
+
activity: dict | None = None,
|
|
247
|
+
complexity_files: list[dict] | None = None,
|
|
248
|
+
tech_debt_files: list[dict] | None = None,
|
|
249
|
+
import_cycles: list[dict] | None = None,
|
|
250
|
+
orphan_modules: list[dict] | None = None,
|
|
251
|
+
deep_nesting_files: list[dict] | None = None,
|
|
252
|
+
total_files: int = 0,
|
|
253
|
+
) -> dict:
|
|
254
|
+
"""Compute an aggregate health score from existing analysis results.
|
|
255
|
+
|
|
256
|
+
All arguments are optional and keyword-only; callers pass whichever
|
|
257
|
+
analyses are available — missing dimensions default to a neutral 50.
|
|
258
|
+
|
|
259
|
+
Returns::
|
|
260
|
+
|
|
261
|
+
{
|
|
262
|
+
"score": int, # 0-100
|
|
263
|
+
"grade": str, # "A" | "B" | "C" | "D" | "F"
|
|
264
|
+
"category_scores": {
|
|
265
|
+
"security": int,
|
|
266
|
+
"test_coverage": int,
|
|
267
|
+
"activity": int,
|
|
268
|
+
"complexity": int,
|
|
269
|
+
"tech_debt": int,
|
|
270
|
+
"architecture": int,
|
|
271
|
+
},
|
|
272
|
+
"weights": {str: float}, # applied weights for transparency
|
|
273
|
+
"strengths": [str, ...], # things going well
|
|
274
|
+
"weaknesses": [str, ...], # things to address
|
|
275
|
+
"notes": [str, ...],
|
|
276
|
+
}
|
|
277
|
+
"""
|
|
278
|
+
cat = {
|
|
279
|
+
"security": _security_score(security),
|
|
280
|
+
"test_coverage": _coverage_score(test_coverage),
|
|
281
|
+
"activity": _activity_score(activity),
|
|
282
|
+
"complexity": _complexity_score(complexity_files, total_files, deep_nesting_files),
|
|
283
|
+
"tech_debt": _tech_debt_score(tech_debt_files, total_files),
|
|
284
|
+
"architecture": _architecture_score(import_cycles, orphan_modules, total_files),
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
weighted = sum(cat[k] * _WEIGHTS[k] for k in _WEIGHTS)
|
|
288
|
+
score = max(0, min(100, round(weighted)))
|
|
289
|
+
grade = _grade(score)
|
|
290
|
+
|
|
291
|
+
strengths, weaknesses = _build_strengths_weaknesses(
|
|
292
|
+
cat,
|
|
293
|
+
security=security,
|
|
294
|
+
test_coverage=test_coverage,
|
|
295
|
+
complexity_files=complexity_files,
|
|
296
|
+
tech_debt_files=tech_debt_files,
|
|
297
|
+
import_cycles=import_cycles,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
notes: list[str] = []
|
|
301
|
+
if total_files == 0:
|
|
302
|
+
notes.append("未扫描到代码文件,部分维度评分使用默认值。")
|
|
303
|
+
if not import_cycles and not orphan_modules:
|
|
304
|
+
notes.append("架构维度基于 import 图分析;上传项目(无 .git)同样支持。")
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
"score": score,
|
|
308
|
+
"grade": grade,
|
|
309
|
+
"category_scores": cat,
|
|
310
|
+
"weights": _WEIGHTS,
|
|
311
|
+
"strengths": strengths,
|
|
312
|
+
"weaknesses": weaknesses,
|
|
313
|
+
"notes": notes,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def render_health_score_markdown(project_name: str, data: dict | None) -> str:
|
|
318
|
+
"""Render the health score as a Markdown section."""
|
|
319
|
+
if not data:
|
|
320
|
+
return ""
|
|
321
|
+
score = data.get("score", 0)
|
|
322
|
+
grade = data.get("grade", "?")
|
|
323
|
+
cat = data.get("category_scores", {})
|
|
324
|
+
|
|
325
|
+
_GRADE_EMOJI = {"A": "🟢", "B": "🟡", "C": "🟠", "D": "🔴", "F": "🔴"}
|
|
326
|
+
emoji = _GRADE_EMOJI.get(grade, "⚪")
|
|
327
|
+
|
|
328
|
+
lines = [
|
|
329
|
+
f"# 健康评分({project_name})",
|
|
330
|
+
"",
|
|
331
|
+
f"> **{score} / 100** — 等级 **{grade}** {emoji}",
|
|
332
|
+
"",
|
|
333
|
+
"## 各维度得分",
|
|
334
|
+
"",
|
|
335
|
+
"| 维度 | 得分 | 权重 |",
|
|
336
|
+
"|------|------|------|",
|
|
337
|
+
]
|
|
338
|
+
_DIM_LABELS = {
|
|
339
|
+
"security": "安全性",
|
|
340
|
+
"test_coverage": "测试覆盖",
|
|
341
|
+
"activity": "活跃度",
|
|
342
|
+
"complexity": "代码复杂度",
|
|
343
|
+
"tech_debt": "技术债",
|
|
344
|
+
"architecture": "架构健康",
|
|
345
|
+
}
|
|
346
|
+
weights = data.get("weights", _WEIGHTS)
|
|
347
|
+
for key, label in _DIM_LABELS.items():
|
|
348
|
+
s = cat.get(key, 50)
|
|
349
|
+
w = int(weights.get(key, 0) * 100)
|
|
350
|
+
lines.append(f"| {label} | {s} | {w}% |")
|
|
351
|
+
|
|
352
|
+
lines += [
|
|
353
|
+
"",
|
|
354
|
+
"> 维度说明(每项满分 100,越高越好):"
|
|
355
|
+
"**安全性**=有没有把密码写死在代码里、用了危险写法等隐患;"
|
|
356
|
+
"**测试覆盖**=有多少代码配了自动测试兜底,改动后能第一时间发现问题;"
|
|
357
|
+
"**活跃度**=最近还有没有人在维护更新;"
|
|
358
|
+
"**代码复杂度**=逻辑绕不绕、新人好不好读懂;"
|
|
359
|
+
"**技术债**=代码里堆了多少迟早要还的「烂账」(TODO、临时凑合的写法);"
|
|
360
|
+
"**架构健康**=文件之间的依赖关系清不清爽、乱不乱。",
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
strengths = data.get("strengths", [])
|
|
364
|
+
if strengths:
|
|
365
|
+
lines += ["", "## 做得好的地方", ""]
|
|
366
|
+
for s in strengths:
|
|
367
|
+
lines.append(f"- {s}")
|
|
368
|
+
|
|
369
|
+
weaknesses = data.get("weaknesses", [])
|
|
370
|
+
if weaknesses:
|
|
371
|
+
lines += ["", "## 建议改进", ""]
|
|
372
|
+
for w in weaknesses:
|
|
373
|
+
lines.append(f"- {w}")
|
|
374
|
+
|
|
375
|
+
notes = data.get("notes", [])
|
|
376
|
+
if notes:
|
|
377
|
+
lines += ["", "## 注意事项", ""]
|
|
378
|
+
for n in notes:
|
|
379
|
+
lines.append(f"- {n}")
|
|
380
|
+
|
|
381
|
+
return "\n".join(lines).rstrip() + "\n"
|