aicortexpy 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 (58) hide show
  1. aicortexpy/__init__.py +3 -0
  2. aicortexpy/__main__.py +5 -0
  3. aicortexpy/acp_client.py +559 -0
  4. aicortexpy/acp_conn.py +853 -0
  5. aicortexpy/acp_errors.py +107 -0
  6. aicortexpy/agent_env.py +88 -0
  7. aicortexpy/artifact_ws.py +233 -0
  8. aicortexpy/artifacts.py +67 -0
  9. aicortexpy/attachments.py +85 -0
  10. aicortexpy/auto_commit.py +106 -0
  11. aicortexpy/capabilities.py +92 -0
  12. aicortexpy/chat_acp_pool.py +321 -0
  13. aicortexpy/cli.py +133 -0
  14. aicortexpy/client.py +438 -0
  15. aicortexpy/codex_config.py +98 -0
  16. aicortexpy/config.py +298 -0
  17. aicortexpy/daemon.py +677 -0
  18. aicortexpy/data/default_skills/agent-memory/SKILL.md +67 -0
  19. aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +144 -0
  20. aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +145 -0
  21. aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +87 -0
  22. aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +96 -0
  23. aicortexpy/data/default_skills/aicortex-basics/SKILL.md +10 -0
  24. aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +118 -0
  25. aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +100 -0
  26. aicortexpy/data/default_skills/aicortex-graph/SKILL.md +30 -0
  27. aicortexpy/data/default_skills/graphify/SKILL.md +97 -0
  28. aicortexpy/data/default_skills/question-form/SKILL.md +162 -0
  29. aicortexpy/data/default_skills/test-report-generator/SKILL.md +179 -0
  30. aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +146 -0
  31. aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +275 -0
  32. aicortexpy/design_seed.py +59 -0
  33. aicortexpy/diskusage.py +209 -0
  34. aicortexpy/execenv.py +264 -0
  35. aicortexpy/gc.py +286 -0
  36. aicortexpy/handoff_bundle.py +65 -0
  37. aicortexpy/health.py +178 -0
  38. aicortexpy/local_skills.py +157 -0
  39. aicortexpy/models.py +385 -0
  40. aicortexpy/openclaw_config.py +153 -0
  41. aicortexpy/pinned_workdir.py +60 -0
  42. aicortexpy/platform_info.py +61 -0
  43. aicortexpy/poisoned.py +44 -0
  44. aicortexpy/process.py +168 -0
  45. aicortexpy/procutil.py +61 -0
  46. aicortexpy/prompt.py +235 -0
  47. aicortexpy/repo.py +462 -0
  48. aicortexpy/runner.py +969 -0
  49. aicortexpy/runtime_config.py +540 -0
  50. aicortexpy/sandbox.py +264 -0
  51. aicortexpy/stream.py +422 -0
  52. aicortexpy/terminal.py +147 -0
  53. aicortexpy/tunnel.py +82 -0
  54. aicortexpy/wakeup.py +222 -0
  55. aicortexpy-0.1.0.dist-info/METADATA +106 -0
  56. aicortexpy-0.1.0.dist-info/RECORD +58 -0
  57. aicortexpy-0.1.0.dist-info/WHEEL +5 -0
  58. aicortexpy-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,146 @@
1
+ # @AI_GENERATED
2
+ """
3
+ 生成 Excel 测试用例清单(.xlsx),按模块分 Sheet。
4
+
5
+ 依赖: pip install openpyxl
6
+
7
+ 用法:
8
+ python generate_cases.py --output "测试用例.xlsx"
9
+
10
+ 复用方式:
11
+ from generate_cases import build_workbook
12
+ build_workbook(modules, "用例.xlsx")
13
+
14
+ modules 结构: {"Sheet名(模块)": [用例dict, ...], ...}
15
+ 用例 dict 字段:
16
+ id 用例编号(多步骤用例仅首行填,续行用 "" 表示延续)
17
+ name 用例名称
18
+ module 所属模块(路径式,如 /需求/模块/子模块/场景)
19
+ precondition 前置条件(可空)
20
+ remark 备注(可空)
21
+ step 步骤描述
22
+ expected 预期结果
23
+ level 用例等级 P0/P1/P2/P3
24
+ owner 责任人(ID)(可选)
25
+ """
26
+ import argparse
27
+
28
+ import openpyxl
29
+ from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
30
+ from openpyxl.utils import get_column_letter
31
+
32
+
33
+ HEADERS = ["ID", "用例名称", "所属模块", "前置条件", "备注",
34
+ "步骤描述", "预期结果", "用例等级", "责任人(ID)"]
35
+ FIELDS = ["id", "name", "module", "precondition", "remark",
36
+ "step", "expected", "level", "owner"]
37
+ # 列宽(字符数)
38
+ WIDTHS = [10, 24, 34, 20, 16, 40, 40, 10, 14]
39
+
40
+ HEADER_FILL = PatternFill("solid", fgColor="4472C4")
41
+ HEADER_FONT = Font(bold=True, color="FFFFFF", size=11)
42
+ BODY_FONT = Font(size=10)
43
+ THIN = Side(style="thin", color="BFBFBF")
44
+ BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
45
+ WRAP_TOP = Alignment(wrap_text=True, vertical="top")
46
+ HEADER_ALIGN = Alignment(horizontal="center", vertical="center")
47
+
48
+ # Excel Sheet 名非法字符与长度限制
49
+ _INVALID = set(r'[]:*?/\\')
50
+
51
+
52
+ def _safe_sheet_name(name, used):
53
+ name = "".join("_" if c in _INVALID else c for c in str(name))[:31] or "Sheet"
54
+ base, i = name, 1
55
+ while name in used:
56
+ suffix = f"_{i}"
57
+ name = base[:31 - len(suffix)] + suffix
58
+ i += 1
59
+ used.add(name)
60
+ return name
61
+
62
+
63
+ def _write_sheet(ws, cases):
64
+ # 表头
65
+ for col, title in enumerate(HEADERS, start=1):
66
+ c = ws.cell(row=1, column=col, value=title)
67
+ c.fill = HEADER_FILL
68
+ c.font = HEADER_FONT
69
+ c.alignment = HEADER_ALIGN
70
+ c.border = BORDER
71
+ # 正文
72
+ for r, case in enumerate(cases, start=2):
73
+ for col, field in enumerate(FIELDS, start=1):
74
+ val = case.get(field, "")
75
+ c = ws.cell(row=r, column=col, value=val)
76
+ c.font = BODY_FONT
77
+ c.alignment = WRAP_TOP
78
+ c.border = BORDER
79
+ # 列宽
80
+ for col, w in enumerate(WIDTHS, start=1):
81
+ ws.column_dimensions[get_column_letter(col)].width = w
82
+ # 冻结表头
83
+ ws.freeze_panes = "A2"
84
+
85
+
86
+ def build_workbook(modules, output_path):
87
+ wb = openpyxl.Workbook()
88
+ wb.remove(wb.active)
89
+ used = set()
90
+ for sheet_name, cases in modules.items():
91
+ ws = wb.create_sheet(_safe_sheet_name(sheet_name, used))
92
+ _write_sheet(ws, cases)
93
+ if not wb.sheetnames: # 防止空工作簿
94
+ wb.create_sheet("用例")
95
+ wb.save(output_path)
96
+ return output_path
97
+
98
+
99
+ # ----------------------------- 示例数据 -----------------------------
100
+ SAMPLE_MODULES = {
101
+ "三方登录注册和绑定": [
102
+ {
103
+ "id": 122946, "name": "取消绑定",
104
+ "module": "/需求/三方登录/APP端/账号绑定/功能",
105
+ "precondition": "", "remark": "",
106
+ "step": "用户进入绑定页面,选择绑定第三方账号,进入第三方账号后点击取消",
107
+ "expected": "返回到 app 绑定页", "level": "P1", "owner": "",
108
+ },
109
+ {
110
+ "id": 122944, "name": "重复绑定",
111
+ "module": "/需求/三方登录/APP端/账号绑定/功能",
112
+ "precondition": "", "remark": "",
113
+ "step": "用一个新的未绑定第三方账号去绑定已绑定的 app 账号",
114
+ "expected": "绑定失败并给出相应提示", "level": "P1", "owner": "",
115
+ },
116
+ ],
117
+ "电价设置": [
118
+ # 多步骤用例示例:首行填 ID,续行 ID/名称/模块/前置留空
119
+ {
120
+ "id": 122737, "name": "设置全时段电价为 1.99",
121
+ "module": "/需求/电价设置/按日期设置电价",
122
+ "precondition": "", "remark": "",
123
+ "step": "点击电价设置入口", "expected": "进入电价设置页",
124
+ "level": "P1", "owner": "",
125
+ },
126
+ {
127
+ "id": "", "name": "", "module": "", "precondition": "", "remark": "",
128
+ "step": "保持默认时间,只设置电价点击保存", "expected": "弹出相应提示",
129
+ "level": "", "owner": "",
130
+ },
131
+ {
132
+ "id": "", "name": "", "module": "", "precondition": "", "remark": "",
133
+ "step": "点击确认", "expected": "电价保存成功",
134
+ "level": "", "owner": "",
135
+ },
136
+ ],
137
+ }
138
+
139
+
140
+ if __name__ == "__main__":
141
+ parser = argparse.ArgumentParser(description="生成 Excel 测试用例清单")
142
+ parser.add_argument("--output", default="测试用例.xlsx", help="输出文件路径")
143
+ args = parser.parse_args()
144
+ path = build_workbook(SAMPLE_MODULES, args.output)
145
+ print(f"已生成测试用例清单: {path}")
146
+ # @AI_GENERATED: end
@@ -0,0 +1,275 @@
1
+ # @AI_GENERATED
2
+ """
3
+ 生成 Word 测试报告(.docx)。
4
+
5
+ 依赖: pip install python-docx
6
+
7
+ 用法:
8
+ python generate_report.py --output "功能测试报告.docx"
9
+
10
+ 复用方式:
11
+ from generate_report import build_report
12
+ build_report(data, "报告.docx")
13
+
14
+ data 结构见文件底部 SAMPLE_DATA。AGENT 应将真实数据填入同样结构后调用 build_report。
15
+ 关键结论字段(缺陷统计、修复率、结论)必须来自真实数据,不得编造。
16
+ """
17
+ import argparse
18
+
19
+ from docx import Document
20
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
21
+ from docx.enum.table import WD_TABLE_ALIGNMENT
22
+ from docx.shared import Pt, RGBColor
23
+ from docx.oxml.ns import qn
24
+
25
+
26
+ # ----------------------------- 样式辅助 -----------------------------
27
+ def _set_cell_bg(cell, hex_color):
28
+ """设置单元格背景色。"""
29
+ tc_pr = cell._tc.get_or_add_tcPr()
30
+ shd = tc_pr.makeelement(qn("w:shd"), {
31
+ qn("w:val"): "clear",
32
+ qn("w:color"): "auto",
33
+ qn("w:fill"): hex_color,
34
+ })
35
+ tc_pr.append(shd)
36
+
37
+
38
+ def _add_table(doc, headers, rows, header_bg="4472C4", header_color=(255, 255, 255)):
39
+ """通用建表:首行表头加背景与加粗。rows 为二维列表。"""
40
+ table = doc.add_table(rows=1, cols=len(headers))
41
+ table.style = "Table Grid"
42
+ table.alignment = WD_TABLE_ALIGNMENT.CENTER
43
+
44
+ hdr = table.rows[0].cells
45
+ for i, text in enumerate(headers):
46
+ hdr[i].text = ""
47
+ p = hdr[i].paragraphs[0]
48
+ run = p.add_run(str(text))
49
+ run.bold = True
50
+ run.font.color.rgb = RGBColor(*header_color)
51
+ _set_cell_bg(hdr[i], header_bg)
52
+
53
+ for row in rows:
54
+ cells = table.add_row().cells
55
+ for i, val in enumerate(row):
56
+ cells[i].text = "" if val is None else str(val)
57
+ doc.add_paragraph()
58
+ return table
59
+
60
+
61
+ def _add_centered(doc, text, size=None, bold=False, space_after=6):
62
+ p = doc.add_paragraph()
63
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
64
+ run = p.add_run(text)
65
+ run.bold = bold
66
+ if size:
67
+ run.font.size = Pt(size)
68
+ p.paragraph_format.space_after = Pt(space_after)
69
+ return p
70
+
71
+
72
+ # ----------------------------- 报告构建 -----------------------------
73
+ def build_report(data, output_path):
74
+ doc = Document()
75
+
76
+ # ---------- 封面 ----------
77
+ for _ in range(6):
78
+ doc.add_paragraph()
79
+ _add_centered(doc, data["system_name"], size=26, bold=True, space_after=12)
80
+ _add_centered(doc, data.get("stage", ""), size=18, bold=True, space_after=12)
81
+ _add_centered(doc, "测试报告", size=22, bold=True, space_after=48)
82
+ for _ in range(6):
83
+ doc.add_paragraph()
84
+ _add_centered(doc, data.get("org", ""), size=14)
85
+ _add_centered(doc, data.get("date", ""), size=14)
86
+ doc.add_page_break()
87
+
88
+ # ---------- 文档修订信息 ----------
89
+ doc.add_heading("文档修订信息", level=1)
90
+ _add_table(
91
+ doc,
92
+ ["版本", "修改内容描述", "修改人", "日期", "审批人"],
93
+ data.get("revisions", []),
94
+ )
95
+ doc.add_page_break()
96
+
97
+ # ---------- 目录 ----------
98
+ doc.add_heading("目录", level=1)
99
+ toc = doc.add_paragraph()
100
+ run = toc.add_run()
101
+ fld = run._r.makeelement(qn("w:fldSimple"), {qn("w:instr"): r"TOC \o \"1-3\" \h \z \u"})
102
+ placeholder = fld.makeelement(qn("w:r"), {})
103
+ t = placeholder.makeelement(qn("w:t"), {})
104
+ t.text = "(打开文档后右键“更新域”生成目录)"
105
+ placeholder.append(t)
106
+ fld.append(placeholder)
107
+ run._r.append(fld)
108
+ doc.add_page_break()
109
+
110
+ # ---------- 1. 概述 ----------
111
+ doc.add_heading("1. 概述", level=1)
112
+ doc.add_heading("1.1 测试目的", level=2)
113
+ doc.add_paragraph(data["overview"]["purpose"])
114
+
115
+ doc.add_heading("1.2 测试范围", level=2)
116
+ _add_table(
117
+ doc,
118
+ ["#", "模块", "核心功能"],
119
+ [[i + 1, m["module"], m["features"]] for i, m in enumerate(data["scope"])],
120
+ )
121
+
122
+ doc.add_heading("1.3 参考资料", level=2)
123
+ for ref in data["overview"].get("references", []):
124
+ doc.add_paragraph(ref, style="List Bullet")
125
+
126
+ # ---------- 2. 测试内容 ----------
127
+ doc.add_heading("2. 测试内容", level=1)
128
+
129
+ doc.add_heading("2.1 测试版本", level=2)
130
+ _add_table(
131
+ doc,
132
+ ["软件名称及组织", "软件版本"],
133
+ [[v["name"], v["version"]] for v in data["versions"]],
134
+ )
135
+
136
+ doc.add_heading("2.2 测试环境", level=2)
137
+ doc.add_paragraph("环境地址:")
138
+ _add_table(
139
+ doc,
140
+ ["环境", "地址"],
141
+ [[e["env"], e["addr"]] for e in data["environment"].get("envs", [])],
142
+ )
143
+ doc.add_paragraph("移动端手机型号以及系统:")
144
+ _add_table(
145
+ doc,
146
+ ["系统", "型号", "版本"],
147
+ [[d["os"], d["model"], d["version"]] for d in data["environment"].get("mobile", [])],
148
+ )
149
+ doc.add_paragraph("Web端浏览器版本:")
150
+ _add_table(
151
+ doc,
152
+ ["浏览器", "版本"],
153
+ [[b["browser"], b["version"]] for b in data["environment"].get("browsers", [])],
154
+ )
155
+
156
+ doc.add_heading("2.3 测试执行", level=2)
157
+ doc.add_heading("2.3.1 功能测试及结果", level=3)
158
+ doc.add_paragraph("核心测试点结果如下:")
159
+ _add_table(
160
+ doc,
161
+ ["#", "模块", "核心功能", "测试结果"],
162
+ [[i + 1, r["module"], r["features"], r["result"]]
163
+ for i, r in enumerate(data["function_results"])],
164
+ )
165
+
166
+ doc.add_heading("2.3.2 集成测试及结果", level=3)
167
+ doc.add_paragraph("核心测试点结果如下:")
168
+ _add_table(
169
+ doc,
170
+ ["ID", "所属模块", "核心测试点", "测试结果"],
171
+ [[r["id"], r["module"], r["point"], r["result"]]
172
+ for r in data.get("integration_results", [])],
173
+ )
174
+
175
+ doc.add_heading("2.3.3 详细测试用例", level=3)
176
+ doc.add_paragraph(
177
+ data.get("detail_cases_note",
178
+ "详细测试用例见配套附件 Excel 用例清单。")
179
+ )
180
+
181
+ # ---------- 3. 测试结论 ----------
182
+ doc.add_heading("3. 测试结论", level=1)
183
+ doc.add_heading("3.1 测试结果", level=2)
184
+ conclusion = data["conclusion"]
185
+ doc.add_paragraph(conclusion["verdict"])
186
+ for para in conclusion.get("summary", []):
187
+ doc.add_paragraph(para)
188
+ doc.add_paragraph("过程缺陷分析如下:")
189
+ d = conclusion["defects"]
190
+ _add_table(
191
+ doc,
192
+ ["问题总数", "严重问题数", "中等问题数", "较轻问题",
193
+ "未解决问题数", "问题解决率", "严重问题比率"],
194
+ [[d["total"], d["critical"], d["medium"], d["minor"],
195
+ d["unresolved"], d["resolve_rate"], d["critical_rate"]]],
196
+ )
197
+
198
+ doc.add_heading("3.2 遗留问题列表", level=2)
199
+ _add_table(
200
+ doc,
201
+ ["模块", "问题描述", "说明"],
202
+ [[i["module"], i["desc"], i["note"]] for i in data.get("open_issues", [])],
203
+ )
204
+
205
+ doc.save(output_path)
206
+ return output_path
207
+
208
+
209
+ # ----------------------------- 示例数据 -----------------------------
210
+ SAMPLE_DATA = {
211
+ "system_name": "示例平台",
212
+ "stage": "P2阶段",
213
+ "org": "示例科技有限公司",
214
+ "date": "2024年07月08日",
215
+ "revisions": [
216
+ ["V1.0", "初始编制", "张三", "2024/7/08", "李四"],
217
+ ],
218
+ "overview": {
219
+ "purpose": "本次报告为示例平台 P2 阶段功能测试报告。测试目的在于验证测试环境"
220
+ "移动端及 web 端部分新增功能以及原有功能的正常使用。",
221
+ "references": ["产品规格说明书.doc", "测试计划.doc"],
222
+ },
223
+ "scope": [
224
+ {"module": "三方登录和绑定", "features": "绑定三方账号 / 登录 / 解绑"},
225
+ {"module": "消息中心", "features": "公告接收 / 已读消息"},
226
+ ],
227
+ "versions": [
228
+ {"name": "前端", "version": "tag_1.0.8_2024070401"},
229
+ {"name": "后端", "version": "tag_1.2.8_240708"},
230
+ ],
231
+ "environment": {
232
+ "envs": [
233
+ {"env": "功能测试环境", "addr": "https://example.com/#/login"},
234
+ ],
235
+ "mobile": [
236
+ {"os": "iOS", "model": "iPhone 14 Pro", "version": "17.5.1"},
237
+ {"os": "Android", "model": "Galaxy A53 5G", "version": "Android 13"},
238
+ ],
239
+ "browsers": [
240
+ {"browser": "Chrome", "version": "126.0.6478.127(64 位)"},
241
+ ],
242
+ },
243
+ "function_results": [
244
+ {"module": "三方登录和绑定", "features": "绑定 / 登录 / 解绑", "result": "Pass"},
245
+ {"module": "消息中心", "features": "公告接收 / 已读", "result": "Pass"},
246
+ ],
247
+ "integration_results": [
248
+ {"id": 1, "module": "公司管理", "point": "创建公司", "result": "Pass"},
249
+ ],
250
+ "detail_cases_note": "详细测试用例见配套附件《示例-测试用例.xlsx》。",
251
+ "conclusion": {
252
+ "verdict": "测试通过。",
253
+ "summary": [
254
+ "本次执行了一轮功能测试、一轮集成测试和两轮回归测试,"
255
+ "共执行新增用例约 730 条,回归用例约 300 条,发现缺陷 110 个。",
256
+ "所有严重及中等缺陷均已修复,整体修复率 98%,达到上线标准。",
257
+ ],
258
+ "defects": {
259
+ "total": 110, "critical": 8, "medium": 65, "minor": 37,
260
+ "unresolved": 2, "resolve_rate": "98%", "critical_rate": "7%",
261
+ },
262
+ },
263
+ "open_issues": [
264
+ {"module": "建站管理", "desc": "欧洲地址建站时区显示错误", "note": "下一版本修复"},
265
+ ],
266
+ }
267
+
268
+
269
+ if __name__ == "__main__":
270
+ parser = argparse.ArgumentParser(description="生成 Word 测试报告")
271
+ parser.add_argument("--output", default="功能测试报告.docx", help="输出文件路径")
272
+ args = parser.parse_args()
273
+ path = build_report(SAMPLE_DATA, args.output)
274
+ print(f"已生成测试报告: {path}")
275
+ # @AI_GENERATED: end
@@ -0,0 +1,59 @@
1
+ """Seed design example bundles into task workdirs (Go design.SeedExample*)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import shutil
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ log = logging.getLogger("aicortexpy.design_seed")
11
+
12
+ MARKER = ".aicortex-design-example"
13
+
14
+
15
+ def seed_example_workdir(
16
+ work_dir: Path,
17
+ example_id: str,
18
+ bundles: list[dict[str, Any]],
19
+ *,
20
+ design_studio: bool = False,
21
+ ) -> None:
22
+ example_id = (example_id or "").strip()
23
+ if not example_id:
24
+ return
25
+ target = work_dir / "design" if design_studio else work_dir
26
+ target.mkdir(parents=True, exist_ok=True)
27
+ marker = target / MARKER
28
+ if marker.is_file() and marker.read_text(encoding="utf-8").strip() == example_id:
29
+ log.debug("design example already seeded id=%s", example_id)
30
+ return
31
+
32
+ for b in bundles:
33
+ if not isinstance(b, dict):
34
+ continue
35
+ src = Path(str(b.get("src") or ""))
36
+ dest_rel = str(b.get("dest") or "").lstrip("/\\")
37
+ if not src.is_file() and not src.is_dir():
38
+ log.warning("design seed skip missing src=%s", src)
39
+ continue
40
+ if not dest_rel or ".." in dest_rel.split("/"):
41
+ log.warning("design seed skip unsafe dest=%s", dest_rel)
42
+ continue
43
+ dest = target / dest_rel
44
+ dest.parent.mkdir(parents=True, exist_ok=True)
45
+ try:
46
+ if src.is_dir():
47
+ if dest.exists():
48
+ shutil.rmtree(dest)
49
+ shutil.copytree(src, dest)
50
+ else:
51
+ shutil.copy2(src, dest)
52
+ except OSError as e:
53
+ log.warning("design seed copy failed %s -> %s: %s", src, dest, e)
54
+
55
+ try:
56
+ marker.write_text(example_id, encoding="utf-8")
57
+ except OSError as e:
58
+ log.warning("design seed marker failed: %s", e)
59
+ log.info("design example seeded id=%s dest=%s", example_id, target)
@@ -0,0 +1,209 @@
1
+ """Read-only workspace disk usage scan (parity with Go diskusage.go)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import time
7
+ from dataclasses import asdict, dataclass, field
8
+ from pathlib import Path
9
+
10
+
11
+ DEFAULT_ARTIFACT_PATTERNS = ("node_modules", ".next", ".turbo", "dist", "build", "__pycache__")
12
+
13
+
14
+ @dataclass
15
+ class TaskDiskUsage:
16
+ workspace_id: str
17
+ workspace_short: str
18
+ task_short: str
19
+ path: str
20
+ kind: str
21
+ parent_status: str
22
+ age_seconds: int
23
+ size_bytes: int
24
+ artifact_size_bytes: int
25
+
26
+
27
+ @dataclass
28
+ class WorkspaceDiskUsage:
29
+ workspace_id: str
30
+ workspace_short: str
31
+ task_count: int
32
+ size_bytes: int
33
+ artifact_size_bytes: int
34
+ artifact_ratio: float
35
+ oldest_age_seconds: int
36
+
37
+
38
+ @dataclass
39
+ class DiskUsageReport:
40
+ workspaces_root: str
41
+ generated_at: str
42
+ artifact_patterns: list[str]
43
+ tasks: list[TaskDiskUsage] = field(default_factory=list)
44
+ workspaces: list[WorkspaceDiskUsage] = field(default_factory=list)
45
+ total_task_count: int = 0
46
+ total_workspace_count: int = 0
47
+ total_size_bytes: int = 0
48
+ total_artifact_size_bytes: int = 0
49
+ total_artifact_ratio: float = 0.0
50
+
51
+
52
+ def scan_disk_usage(
53
+ workspaces_root: Path | str,
54
+ artifact_patterns: list[str] | None = None,
55
+ ) -> DiskUsageReport:
56
+ root = Path(workspaces_root)
57
+ patterns = _pattern_set(artifact_patterns or list(DEFAULT_ARTIFACT_PATTERNS))
58
+ report = DiskUsageReport(
59
+ workspaces_root=str(root),
60
+ generated_at=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
61
+ artifact_patterns=sorted(patterns),
62
+ )
63
+ if not root.is_dir():
64
+ return report
65
+
66
+ ws_agg: dict[str, WorkspaceDiskUsage] = {}
67
+ now = time.time()
68
+
69
+ for ws_entry in root.iterdir():
70
+ if not ws_entry.is_dir() or ws_entry.name == ".repos":
71
+ continue
72
+ ws_id = ws_entry.name
73
+ for task_entry in ws_entry.iterdir():
74
+ if not task_entry.is_dir():
75
+ continue
76
+ # Skip pinned projects tree at workspace/projects/
77
+ if task_entry.name == "projects":
78
+ continue
79
+ usage = _task_usage(task_entry, ws_id, patterns, now)
80
+ report.tasks.append(usage)
81
+ report.total_size_bytes += usage.size_bytes
82
+ report.total_artifact_size_bytes += usage.artifact_size_bytes
83
+ agg = ws_agg.get(ws_id)
84
+ if agg is None:
85
+ agg = WorkspaceDiskUsage(
86
+ workspace_id=ws_id,
87
+ workspace_short=ws_id[:8],
88
+ task_count=0,
89
+ size_bytes=0,
90
+ artifact_size_bytes=0,
91
+ artifact_ratio=0.0,
92
+ oldest_age_seconds=usage.age_seconds,
93
+ )
94
+ ws_agg[ws_id] = agg
95
+ agg.task_count += 1
96
+ agg.size_bytes += usage.size_bytes
97
+ agg.artifact_size_bytes += usage.artifact_size_bytes
98
+ if usage.age_seconds > agg.oldest_age_seconds:
99
+ agg.oldest_age_seconds = usage.age_seconds
100
+
101
+ workspaces = list(ws_agg.values())
102
+ for ws in workspaces:
103
+ ws.artifact_ratio = (
104
+ (ws.artifact_size_bytes / ws.size_bytes) if ws.size_bytes else 0.0
105
+ )
106
+ workspaces.sort(key=lambda w: w.size_bytes, reverse=True)
107
+ report.workspaces = workspaces
108
+ report.total_task_count = len(report.tasks)
109
+ report.total_workspace_count = len(workspaces)
110
+ report.total_artifact_ratio = (
111
+ (report.total_artifact_size_bytes / report.total_size_bytes)
112
+ if report.total_size_bytes
113
+ else 0.0
114
+ )
115
+ report.tasks.sort(key=lambda t: t.size_bytes, reverse=True)
116
+ return report
117
+
118
+
119
+ def report_to_dict(report: DiskUsageReport) -> dict:
120
+ return asdict(report)
121
+
122
+
123
+ def format_report_table(report: DiskUsageReport, *, top: int = 20) -> str:
124
+ lines = [
125
+ f"workspaces_root: {report.workspaces_root}",
126
+ f"total: tasks={report.total_task_count} workspaces={report.total_workspace_count} "
127
+ f"size={_fmt_bytes(report.total_size_bytes)} "
128
+ f"artifacts={_fmt_bytes(report.total_artifact_size_bytes)} "
129
+ f"({report.total_artifact_ratio:.0%})",
130
+ "",
131
+ f"{'SIZE':>10} {'ART':>10} {'AGE':>8} PATH",
132
+ ]
133
+ for t in report.tasks[: max(1, top)]:
134
+ lines.append(
135
+ f"{_fmt_bytes(t.size_bytes):>10} {_fmt_bytes(t.artifact_size_bytes):>10} "
136
+ f"{_fmt_age(t.age_seconds):>8} {t.path}"
137
+ )
138
+ return "\n".join(lines)
139
+
140
+
141
+ def _pattern_set(patterns: list[str]) -> set[str]:
142
+ out: set[str] = set()
143
+ for p in patterns:
144
+ name = Path(str(p)).name.strip()
145
+ if name and "/" not in name and "\\" not in name and name not in (".", ".."):
146
+ out.add(name)
147
+ return out
148
+
149
+
150
+ def _task_usage(task_dir: Path, ws_id: str, patterns: set[str], now: float) -> TaskDiskUsage:
151
+ size = 0
152
+ art = 0
153
+ for path in task_dir.rglob("*"):
154
+ try:
155
+ if path.is_symlink() or not path.is_file():
156
+ continue
157
+ if ".git" in path.parts:
158
+ continue
159
+ n = path.stat().st_size
160
+ except OSError:
161
+ continue
162
+ size += n
163
+ if any(part in patterns for part in path.parts):
164
+ art += n
165
+ kind = "unknown"
166
+ parent_status = ""
167
+ meta = task_dir / ".gc_meta.json"
168
+ if meta.is_file():
169
+ try:
170
+ data = json.loads(meta.read_text(encoding="utf-8"))
171
+ kind = str(data.get("kind") or kind)
172
+ parent_status = str(data.get("parent_status") or "")
173
+ except (OSError, json.JSONDecodeError):
174
+ pass
175
+ try:
176
+ mtime = task_dir.stat().st_mtime
177
+ except OSError:
178
+ mtime = now
179
+ age = max(0, int(now - mtime))
180
+ return TaskDiskUsage(
181
+ workspace_id=ws_id,
182
+ workspace_short=ws_id[:8],
183
+ task_short=task_dir.name[:8],
184
+ path=str(task_dir),
185
+ kind=kind,
186
+ parent_status=parent_status,
187
+ age_seconds=age,
188
+ size_bytes=size,
189
+ artifact_size_bytes=art,
190
+ )
191
+
192
+
193
+ def _fmt_bytes(n: int) -> str:
194
+ x = float(n)
195
+ for unit in ("B", "KB", "MB", "GB", "TB"):
196
+ if abs(x) < 1024 or unit == "TB":
197
+ if unit == "B":
198
+ return f"{int(x)}{unit}"
199
+ return f"{x:.1f}{unit}"
200
+ x /= 1024
201
+ return f"{n}B"
202
+
203
+
204
+ def _fmt_age(seconds: int) -> str:
205
+ if seconds < 3600:
206
+ return f"{seconds // 60}m"
207
+ if seconds < 86400:
208
+ return f"{seconds // 3600}h"
209
+ return f"{seconds // 86400}d"