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.
Files changed (62) hide show
  1. backend/__init__.py +0 -0
  2. backend/app.py +95 -0
  3. backend/desktop_server.py +46 -0
  4. backend/models.py +629 -0
  5. backend/prompts/__init__.py +0 -0
  6. backend/prompts/annotate.py +44 -0
  7. backend/prompts/edit.py +62 -0
  8. backend/prompts/overview.py +57 -0
  9. backend/prompts/qa.py +45 -0
  10. backend/routers/__init__.py +0 -0
  11. backend/routers/analyze.py +271 -0
  12. backend/routers/project.py +872 -0
  13. backend/services/__init__.py +0 -0
  14. backend/services/action_plan.py +366 -0
  15. backend/services/activity.py +313 -0
  16. backend/services/apimap.py +391 -0
  17. backend/services/async_surface.py +123 -0
  18. backend/services/cache.py +183 -0
  19. backend/services/churn.py +244 -0
  20. backend/services/ci_checks.py +317 -0
  21. backend/services/codemap_export.py +136 -0
  22. backend/services/commands.py +276 -0
  23. backend/services/complexity.py +111 -0
  24. backend/services/config_files.py +190 -0
  25. backend/services/contributing.py +308 -0
  26. backend/services/coverage.py +283 -0
  27. backend/services/datamodels.py +190 -0
  28. backend/services/deep_nesting.py +110 -0
  29. backend/services/dependencies.py +270 -0
  30. backend/services/docs.py +255 -0
  31. backend/services/docstrings.py +163 -0
  32. backend/services/duplicate_code.py +165 -0
  33. backend/services/entrypoints.py +152 -0
  34. backend/services/envscan.py +167 -0
  35. backend/services/error_handling.py +239 -0
  36. backend/services/filenames.py +261 -0
  37. backend/services/github_clone.py +129 -0
  38. backend/services/glossary.py +96 -0
  39. backend/services/health_score.py +381 -0
  40. backend/services/importgraph.py +986 -0
  41. backend/services/integrations.py +263 -0
  42. backend/services/licenses.py +505 -0
  43. backend/services/llm.py +117 -0
  44. backend/services/long_functions.py +101 -0
  45. backend/services/ownership.py +194 -0
  46. backend/services/package_purposes.py +112 -0
  47. backend/services/pr_reading.py +181 -0
  48. backend/services/release_map.py +402 -0
  49. backend/services/report_export.py +232 -0
  50. backend/services/risk.py +88 -0
  51. backend/services/scanner.py +546 -0
  52. backend/services/schedules.py +369 -0
  53. backend/services/security.py +445 -0
  54. backend/services/settings_map.py +234 -0
  55. backend/services/symbols.py +605 -0
  56. backend/services/techdebt.py +124 -0
  57. backend/services/too_many_params.py +102 -0
  58. backend/services/typing_coverage.py +184 -0
  59. codeabc-0.1.0.dist-info/METADATA +283 -0
  60. codeabc-0.1.0.dist-info/RECORD +62 -0
  61. codeabc-0.1.0.dist-info/WHEEL +4 -0
  62. codeabc-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,369 @@
1
+ """Scheduled & automated tasks map — what this project runs on its own.
2
+
3
+ Entry points answer "how do I *start* this myself" and CLI commands answer
4
+ "what verbs can I *type*". This answers a question a non-programmer rarely thinks
5
+ to ask but very much wants the answer to: **does this project do anything by
6
+ itself — on a timer, on a schedule, automatically?** A repo that quietly emails a
7
+ report every morning, retries a queue every 30 seconds, or runs a CI job nightly
8
+ behaves very differently from one that only ever acts when you press a button,
9
+ and that difference is invisible unless you know the handful of libraries people
10
+ use to wire it up.
11
+
12
+ This collects those automated triggers straight from the source CodeABC already
13
+ loaded — no LLM, nothing to install — by recognising the common scheduling
14
+ mechanisms across the stacks CodeABC sees most:
15
+
16
+ GitHub Actions ``on: schedule: - cron: "*/5 * * * *"`` (.github/workflows)
17
+ APScheduler ``@scheduler.scheduled_job("cron", hour=0)`` / ``add_job(...)``
18
+ Celery beat ``@periodic_task(...)`` / ``crontab(...)`` / ``beat_schedule``
19
+ schedule (lib) ``schedule.every(10).minutes.do(job)``
20
+ FastAPI ``@repeat_every(seconds=3600)``
21
+ node-cron ``cron.schedule("0 * * * *", task)``
22
+ NestJS ``@Cron(...)`` / ``@Interval(...)`` / ``@Timeout(...)``
23
+ Browser/Node ``setInterval(fn, 5000)``
24
+
25
+ For each hit it reports *what* runs, by *which* mechanism, *how often* (the raw
26
+ schedule as written) and — the文科生-friendly touch — a plain-language gloss of
27
+ common cron expressions (``*/5 * * * *`` → ``每 5 分钟``).
28
+
29
+ :func:`find_scheduled_tasks` is pure over the file contents, so it is
30
+ unit-testable with plain strings and needs no repository.
31
+
32
+ Limitations (kept honest on purpose):
33
+
34
+ * Pattern-based, not a full parse. A schedule whose decorator arguments span
35
+ several lines, or one built dynamically at runtime, may be missed.
36
+ * ``setInterval`` is reported as a recurring timer; a bare ``setTimeout``
37
+ (usually a one-shot delay) is intentionally not, to avoid flooding the map.
38
+ * The plain-language gloss only covers the common cron shapes; anything
39
+ unusual is shown verbatim without a translation rather than guessed at.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import re
45
+
46
+ _PY_SUFFIX = ".py"
47
+ _JS_SUFFIXES = (".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs")
48
+ _YAML_SUFFIXES = (".yml", ".yaml")
49
+
50
+ # A trailing function/method captured after one or more stacked decorators.
51
+ _DEF_TAIL = r"[^\n]*\n(?:[ \t]*@[^\n]*\n)*[ \t]*(?:async[ \t]+)?def[ \t]+(\w+)"
52
+
53
+ # --- GitHub Actions / any YAML cron: key -----------------------------------
54
+ # Matches `- cron: "*/5 * * * *"` and unquoted forms; the backref keeps the
55
+ # quotes balanced so a trailing `# comment` is not swallowed into the value.
56
+ _YAML_CRON = re.compile(
57
+ r"""^[ \t]*-?[ \t]*cron:[ \t]*(['"]?)([^\n#]*?)\1[ \t]*(?:#.*)?$""",
58
+ re.MULTILINE,
59
+ )
60
+
61
+ # --- Python --------------------------------------------------------------
62
+ _APS_DECORATOR = re.compile(r"@[\w.]*?scheduled_job\(([^)]*)\)" + _DEF_TAIL)
63
+ _PERIODIC_TASK = re.compile(r"@(?:[\w.]+\.)?periodic_task\(([^)]*)\)" + _DEF_TAIL)
64
+ _REPEAT_EVERY = re.compile(r"@(?:[\w.]+\.)?repeat_every\(([^)]*)\)" + _DEF_TAIL)
65
+ _ADD_JOB = re.compile(r"\.add_job\(\s*([\w.]+)?")
66
+ _SCHEDULE_LIB = re.compile(
67
+ r"\bschedule\.every\(([^)]*)\)((?:\.\w+(?:\([^)]*\))?)+?)\.do\(\s*([\w.]+)?"
68
+ )
69
+ _CRONTAB = re.compile(r"\bcrontab\(([^)]*)\)")
70
+ _BEAT_SCHEDULE = re.compile(r"\bbeat_schedule\b\s*=\s*\{")
71
+
72
+ # --- JS / TS -------------------------------------------------------------
73
+ _NODE_CRON = re.compile(r"""\bcron\.schedule\(\s*(['"`])([^'"`]+)\1""")
74
+ _NEST = re.compile(
75
+ r"@(Cron|Interval|Timeout)\(([^)]*)\)"
76
+ r"[^\n]*\n(?:[ \t]*@[^\n]*\n)*[ \t]*"
77
+ r"(?:public |private |protected |async |readonly )*(\w+)\s*\("
78
+ )
79
+ _SET_INTERVAL = re.compile(r"\bsetInterval\b\s*\(([^,]*),\s*(\d[\d_]*)?")
80
+
81
+ _MECH_LABEL = {
82
+ "github-actions": "GitHub Actions 定时",
83
+ "apscheduler": "APScheduler",
84
+ "celery": "Celery 定时",
85
+ "schedule": "schedule 库",
86
+ "repeat-every": "FastAPI 周期任务",
87
+ "node-cron": "node-cron",
88
+ "nestjs": "NestJS 定时",
89
+ "interval": "setInterval 定时器",
90
+ }
91
+
92
+ _SCHEDULE_UNIT = {
93
+ "second": "秒",
94
+ "seconds": "秒",
95
+ "minute": "分钟",
96
+ "minutes": "分钟",
97
+ "hour": "小时",
98
+ "hours": "小时",
99
+ "day": "天",
100
+ "days": "天",
101
+ "week": "周",
102
+ "weeks": "周",
103
+ }
104
+
105
+
106
+ def _line_of(content: str, pos: int) -> int:
107
+ return content.count("\n", 0, pos) + 1
108
+
109
+
110
+ def _stem(path: str) -> str:
111
+ base = path.replace("\\", "/").rsplit("/", 1)[-1]
112
+ return base.rsplit(".", 1)[0] or base
113
+
114
+
115
+ def _cron_to_human(expr: str) -> str:
116
+ """Plain-language gloss of the common 5-field cron shapes, else ``""``."""
117
+ parts = expr.split()
118
+ if len(parts) != 5:
119
+ return ""
120
+ minute, hour, dom, month, dow = parts
121
+ star_rest = dom == month == dow == "*"
122
+
123
+ if parts == ["*", "*", "*", "*", "*"]:
124
+ return "每分钟"
125
+ m = re.fullmatch(r"\*/(\d+)", minute)
126
+ if m and hour == "*" and star_rest:
127
+ return f"每 {int(m.group(1))} 分钟"
128
+ if hour == "*" and star_rest and minute.isdigit():
129
+ return "每小时整点" if minute == "0" else f"每小时第 {int(minute)} 分"
130
+ mh = re.fullmatch(r"\*/(\d+)", hour)
131
+ if mh and star_rest and minute.isdigit():
132
+ head = f"每 {int(mh.group(1))} 小时"
133
+ return head if minute == "0" else head + f"第 {int(minute)} 分"
134
+ if minute.isdigit() and hour.isdigit() and star_rest:
135
+ return f"每天 {int(hour):02d}:{int(minute):02d}"
136
+ if minute.isdigit() and hour.isdigit() and dom == "*" and month == "*" and dow.isdigit():
137
+ names = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]
138
+ return f"每{names[int(dow) % 7]} {int(hour):02d}:{int(minute):02d}"
139
+ if minute.isdigit() and hour.isdigit() and dom.isdigit() and month == "*" and dow == "*":
140
+ return f"每月 {int(dom)} 日 {int(hour):02d}:{int(minute):02d}"
141
+ return ""
142
+
143
+
144
+ def _amount_to_human(amount: int, unit_zh: str) -> str:
145
+ return f"每 {amount} {unit_zh}" if amount != 1 else f"每{unit_zh}"
146
+
147
+
148
+ def _seconds_to_human(raw: str) -> str:
149
+ try:
150
+ n = int(raw)
151
+ except ValueError:
152
+ return ""
153
+ if n % 86400 == 0:
154
+ return _amount_to_human(n // 86400, "天")
155
+ if n % 3600 == 0:
156
+ return _amount_to_human(n // 3600, "小时")
157
+ if n % 60 == 0:
158
+ return _amount_to_human(n // 60, "分钟")
159
+ return _amount_to_human(n, "秒")
160
+
161
+
162
+ def _ms_to_human(raw: str) -> str:
163
+ digits = raw.replace("_", "")
164
+ if not digits.isdigit():
165
+ return ""
166
+ n = int(digits)
167
+ if n % 3_600_000 == 0:
168
+ return _amount_to_human(n // 3_600_000, "小时")
169
+ if n % 60_000 == 0:
170
+ return _amount_to_human(n // 60_000, "分钟")
171
+ if n % 1000 == 0:
172
+ return _amount_to_human(n // 1000, "秒")
173
+ return _amount_to_human(n, "毫秒")
174
+
175
+
176
+ _APS_TIMING_KW = frozenset(
177
+ {
178
+ "seconds",
179
+ "minutes",
180
+ "hours",
181
+ "days",
182
+ "weeks",
183
+ "second",
184
+ "minute",
185
+ "hour",
186
+ "day",
187
+ "day_of_week",
188
+ "month",
189
+ "week",
190
+ }
191
+ )
192
+
193
+
194
+ def _aps_trigger(text: str) -> str:
195
+ """Summarise an APScheduler trigger spec (positional kind + timing kwargs).
196
+
197
+ Timing kwargs are emitted in source order so the gloss reads the way the
198
+ author wrote it (``cron, hour=0, minute=30``), not in a fixed reshuffle.
199
+ """
200
+ parts: list[str] = []
201
+ kind = re.search(r"""['"](cron|interval|date)['"]""", text)
202
+ if kind:
203
+ parts.append(kind.group(1))
204
+ for km in re.finditer(r"\b(\w+)\s*=\s*([^\s,)]+)", text):
205
+ if km.group(1) in _APS_TIMING_KW:
206
+ parts.append(f"{km.group(1)}={km.group(2)}")
207
+ return ", ".join(parts)
208
+
209
+
210
+ def _find_in_yaml(path: str, content: str) -> list[dict]:
211
+ found: list[dict] = []
212
+ name = _stem(path)
213
+ for m in _YAML_CRON.finditer(content):
214
+ expr = m.group(2).strip()
215
+ if not expr:
216
+ continue
217
+ found.append(
218
+ {
219
+ "name": name,
220
+ "mechanism": "github-actions",
221
+ "schedule": expr,
222
+ "schedule_human": _cron_to_human(expr),
223
+ "line": _line_of(content, m.start()),
224
+ }
225
+ )
226
+ return found
227
+
228
+
229
+ def _find_in_python(content: str) -> list[dict]:
230
+ found: list[dict] = []
231
+
232
+ for m in _APS_DECORATOR.finditer(content):
233
+ found.append(
234
+ _task(m.group(2), "apscheduler", _aps_trigger(m.group(1)), "", content, m.start())
235
+ )
236
+ for m in _PERIODIC_TASK.finditer(content):
237
+ found.append(_task(m.group(2), "celery", _aps_trigger(m.group(1)), "", content, m.start()))
238
+ for m in _REPEAT_EVERY.finditer(content):
239
+ secs = re.search(r"seconds\s*=\s*([\d_]+)", m.group(1))
240
+ raw = m.group(1).strip()
241
+ human = _seconds_to_human(secs.group(1).replace("_", "")) if secs else ""
242
+ found.append(_task(m.group(2), "repeat-every", raw, human, content, m.start()))
243
+ for m in _ADD_JOB.finditer(content):
244
+ window = content[m.start() : m.start() + 220]
245
+ name = m.group(1) or "add_job 任务"
246
+ found.append(_task(name, "apscheduler", _aps_trigger(window), "", content, m.start()))
247
+ for m in _SCHEDULE_LIB.finditer(content):
248
+ amount, chain, target = m.group(1).strip(), m.group(2), m.group(3)
249
+ sched = f"every({amount}){chain}"
250
+ unit = chain.lstrip(".").split(".")[0]
251
+ human = ""
252
+ if amount.isdigit() and unit in _SCHEDULE_UNIT:
253
+ human = _amount_to_human(int(amount), _SCHEDULE_UNIT[unit])
254
+ found.append(_task(target or "schedule 任务", "schedule", sched, human, content, m.start()))
255
+ for m in _CRONTAB.finditer(content):
256
+ args = m.group(1).strip()
257
+ found.append(_task("Celery 周期任务", "celery", f"crontab({args})", "", content, m.start()))
258
+ for m in _BEAT_SCHEDULE.finditer(content):
259
+ found.append(_task("周期任务表 (beat_schedule)", "celery", "", "", content, m.start()))
260
+
261
+ return found
262
+
263
+
264
+ def _find_in_js(content: str) -> list[dict]:
265
+ found: list[dict] = []
266
+ for m in _NODE_CRON.finditer(content):
267
+ expr = m.group(2).strip()
268
+ found.append(
269
+ _task("node-cron 任务", "node-cron", expr, _cron_to_human(expr), content, m.start())
270
+ )
271
+ for m in _NEST.finditer(content):
272
+ decorator, args, method = m.group(1), m.group(2).strip(), m.group(3)
273
+ sched, human = _nest_schedule(decorator, args)
274
+ found.append(_task(method, "nestjs", sched, human, content, m.start()))
275
+ for m in _SET_INTERVAL.finditer(content):
276
+ ms = m.group(2)
277
+ human = _ms_to_human(ms) if ms else ""
278
+ sched = f"{ms} ms" if ms else ""
279
+ found.append(_task("周期定时器", "interval", sched, human, content, m.start()))
280
+ return found
281
+
282
+
283
+ def _nest_schedule(decorator: str, args: str) -> tuple[str, str]:
284
+ quoted = re.search(r"""['"]([^'"]+)['"]""", args)
285
+ if quoted and len(quoted.group(1).split()) == 5:
286
+ expr = quoted.group(1)
287
+ return expr, _cron_to_human(expr)
288
+ if decorator in ("Interval", "Timeout"):
289
+ num = re.search(r"\b(\d[\d_]*)\b", args)
290
+ if num:
291
+ return f"{decorator}({args})", _ms_to_human(num.group(1))
292
+ return args or decorator, ""
293
+
294
+
295
+ def _task(name: str, mechanism: str, schedule: str, human: str, content: str, pos: int) -> dict:
296
+ return {
297
+ "name": name,
298
+ "mechanism": mechanism,
299
+ "schedule": schedule,
300
+ "schedule_human": human,
301
+ "line": _line_of(content, pos),
302
+ }
303
+
304
+
305
+ def find_scheduled_tasks(file_contents: dict[str, str], *, limit: int = 50) -> dict:
306
+ """Collect the automated / scheduled triggers a project wires up.
307
+
308
+ Args:
309
+ file_contents: mapping of path to file text (as CodeABC already read it).
310
+ limit: how many tasks to return in the sorted list.
311
+
312
+ Returns ``{"total", "mechanisms", "tasks"}`` where each task is
313
+ ``{"name", "mechanism", "schedule", "schedule_human", "path", "line"}``.
314
+ """
315
+ seen: set[tuple] = set()
316
+ tasks: list[dict] = []
317
+
318
+ for path, content in file_contents.items():
319
+ if not content:
320
+ continue
321
+ if path.endswith(_YAML_SUFFIXES):
322
+ entries = _find_in_yaml(path, content)
323
+ elif path.endswith(_PY_SUFFIX):
324
+ entries = _find_in_python(content)
325
+ elif path.endswith(_JS_SUFFIXES):
326
+ entries = _find_in_js(content)
327
+ else:
328
+ continue
329
+
330
+ for entry in entries:
331
+ key = (path, entry["line"], entry["name"], entry["mechanism"])
332
+ if key in seen:
333
+ continue
334
+ seen.add(key)
335
+ tasks.append({**entry, "path": path})
336
+
337
+ tasks.sort(key=lambda t: (t["path"], t["line"]))
338
+ mechanisms = sorted({t["mechanism"] for t in tasks})
339
+ return {"total": len(tasks), "mechanisms": mechanisms, "tasks": tasks[:limit]}
340
+
341
+
342
+ def render_schedules_markdown(project_name: str, data: dict | None) -> str:
343
+ """Render the scheduled-tasks map as Markdown, or ``""`` if none were found."""
344
+ tasks = (data or {}).get("tasks") or []
345
+ if not tasks:
346
+ return ""
347
+
348
+ lines = [
349
+ f"# {project_name} — 会自己定时跑的任务(自动化 / 定时)",
350
+ "",
351
+ "> 入口是“我怎么手动启动”,这里是“项目会不会自己定时做点什么”——"
352
+ "比如每天发一封报告、每 30 秒重试一次队列、每晚跑一次 CI。"
353
+ "这类任务不用你按按钮就会发生,看清它跑什么、多久跑一次,再决定要不要改。",
354
+ "",
355
+ ]
356
+ current_path = None
357
+ for task in tasks:
358
+ if task["path"] != current_path:
359
+ current_path = task["path"]
360
+ lines.append(f"## `{current_path}`")
361
+ label = _MECH_LABEL.get(task["mechanism"], task["mechanism"])
362
+ bits = [f"- `{task['name']}` — {label}"]
363
+ if task["schedule"]:
364
+ bits.append(f":`{task['schedule']}`")
365
+ if task["schedule_human"]:
366
+ bits.append(f"({task['schedule_human']})")
367
+ bits.append(f" 第 {task['line']} 行")
368
+ lines.append("".join(bits))
369
+ return "\n".join(lines).rstrip() + "\n"