jarvis-ai-assistant 0.5.0__py3-none-any.whl → 0.6.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 (41) hide show
  1. jarvis/__init__.py +1 -1
  2. jarvis/jarvis_agent/__init__.py +114 -6
  3. jarvis/jarvis_agent/agent_manager.py +3 -0
  4. jarvis/jarvis_agent/jarvis.py +45 -9
  5. jarvis/jarvis_agent/run_loop.py +6 -1
  6. jarvis/jarvis_agent/task_planner.py +219 -0
  7. jarvis/jarvis_c2rust/__init__.py +13 -0
  8. jarvis/jarvis_c2rust/cli.py +405 -0
  9. jarvis/jarvis_c2rust/collector.py +209 -0
  10. jarvis/jarvis_c2rust/library_replacer.py +933 -0
  11. jarvis/jarvis_c2rust/llm_module_agent.py +1265 -0
  12. jarvis/jarvis_c2rust/scanner.py +1671 -0
  13. jarvis/jarvis_c2rust/transpiler.py +1236 -0
  14. jarvis/jarvis_code_agent/code_agent.py +151 -18
  15. jarvis/jarvis_data/config_schema.json +13 -3
  16. jarvis/jarvis_sec/README.md +180 -0
  17. jarvis/jarvis_sec/__init__.py +674 -0
  18. jarvis/jarvis_sec/checkers/__init__.py +33 -0
  19. jarvis/jarvis_sec/checkers/c_checker.py +1269 -0
  20. jarvis/jarvis_sec/checkers/rust_checker.py +367 -0
  21. jarvis/jarvis_sec/cli.py +110 -0
  22. jarvis/jarvis_sec/prompts.py +324 -0
  23. jarvis/jarvis_sec/report.py +260 -0
  24. jarvis/jarvis_sec/types.py +20 -0
  25. jarvis/jarvis_sec/workflow.py +513 -0
  26. jarvis/jarvis_tools/cli/main.py +1 -0
  27. jarvis/jarvis_tools/execute_script.py +1 -1
  28. jarvis/jarvis_tools/read_code.py +11 -1
  29. jarvis/jarvis_tools/read_symbols.py +129 -0
  30. jarvis/jarvis_tools/registry.py +9 -1
  31. jarvis/jarvis_tools/sub_agent.py +4 -3
  32. jarvis/jarvis_tools/sub_code_agent.py +3 -3
  33. jarvis/jarvis_utils/config.py +28 -6
  34. jarvis/jarvis_utils/git_utils.py +39 -0
  35. jarvis/jarvis_utils/utils.py +150 -7
  36. {jarvis_ai_assistant-0.5.0.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/METADATA +13 -1
  37. {jarvis_ai_assistant-0.5.0.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/RECORD +41 -22
  38. {jarvis_ai_assistant-0.5.0.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/entry_points.txt +4 -0
  39. {jarvis_ai_assistant-0.5.0.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/WHEEL +0 -0
  40. {jarvis_ai_assistant-0.5.0.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/licenses/LICENSE +0 -0
  41. {jarvis_ai_assistant-0.5.0.dist-info → jarvis_ai_assistant-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1265 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ LLM 驱动的 Rust Crate 模块规划 Agent
4
+
5
+ 目标:
6
+ - 复用 scanner 中的 find_root_function_ids 与调用图信息,构造“以根函数为起点”的上下文
7
+ - 通过 jarvis_agent.Agent 调用 LLM,基于上下文生成 Rust crate 的目录规划(YAML)
8
+
9
+ 设计要点:
10
+ - 与现有 scanner/cli 解耦,最小侵入新增模块
11
+ - 使用 jarvis_agent.Agent 的平台与系统提示管理能力,但不走完整工具循环,直接进行一次性对话生成
12
+ - 对输出格式进行强约束:仅输出 YAML,无解释文本
13
+
14
+ 用法:
15
+ from jarvis.jarvis_c2rust.llm_module_agent import plan_crate_yaml_llm
16
+ print(plan_crate_yaml_llm(project_root="."))
17
+
18
+ CLI 集成建议:
19
+ 可在 jarvis_c2rust/cli.py 中新增 llm-plan 子命令调用本模块的 plan_crate_yaml_llm(已独立封装,便于后续补充)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ # removed sqlite3 (migrated to JSONL/JSON)
26
+ from dataclasses import dataclass
27
+ from pathlib import Path
28
+ from typing import Any, Dict, List, Optional, Set, Tuple, Union
29
+ import re
30
+
31
+ from jarvis.jarvis_c2rust.scanner import find_root_function_ids
32
+ from jarvis.jarvis_agent import Agent # 复用 LLM Agent 能力
33
+ from jarvis.jarvis_utils.input import user_confirm
34
+
35
+
36
+ @dataclass
37
+ class _FnMeta:
38
+ id: int
39
+ name: str
40
+ qname: str
41
+ signature: str
42
+ file: str
43
+ refs: List[str]
44
+
45
+ @property
46
+ def label(self) -> str:
47
+ base = self.qname or self.name or f"fn_{self.id}"
48
+ if self.signature and self.signature != base:
49
+ return f"{base}\n{self.signature}"
50
+ return base
51
+
52
+ @property
53
+ def top_namespace(self) -> str:
54
+ """
55
+ 提取顶层命名空间/类名:
56
+ - qualified_name 形如 ns1::ns2::Class::method -> 返回 ns1
57
+ - C 函数或无命名空间 -> 返回 "c"
58
+ """
59
+ if self.qname and "::" in self.qname:
60
+ return self.qname.split("::", 1)[0] or "c"
61
+ return "c"
62
+
63
+
64
+ def _sanitize_mod_name(s: str) -> str:
65
+ s = (s or "").replace("::", "__")
66
+ safe = []
67
+ for ch in s:
68
+ if ch.isalnum() or ch == "_":
69
+ safe.append(ch.lower())
70
+ else:
71
+ safe.append("_")
72
+ out = "".join(safe).strip("_")
73
+ return out[:80] or "mod"
74
+
75
+
76
+ class _GraphLoader:
77
+ """
78
+ 仅从 symbols.jsonl 读取符号与调用关系,提供子图遍历能力:
79
+ - 数据源:<project_root>/.jarvis/c2rust/symbols.jsonl 或显式传入的 .jsonl 文件
80
+ - 不再支持任何回退策略(不考虑 symbols_raw.jsonl、functions.jsonl 等)
81
+ """
82
+
83
+ def __init__(self, db_path: Path, project_root: Path):
84
+ self.project_root = Path(project_root).resolve()
85
+
86
+ def _resolve_data_path(hint: Path) -> Path:
87
+ p = Path(hint)
88
+ # 仅支持 symbols.jsonl;不再兼容 functions.jsonl 或其他旧格式
89
+ # 若直接传入文件路径且为 .jsonl,则直接使用(要求内部包含 category/ref 字段)
90
+ if p.is_file() and p.suffix.lower() == ".jsonl":
91
+ return p
92
+ # 目录:仅支持 <dir>/.jarvis/c2rust/symbols.jsonl
93
+ if p.is_dir():
94
+ return p / ".jarvis" / "c2rust" / "symbols.jsonl"
95
+ # 默认:项目 .jarvis/c2rust/symbols.jsonl
96
+ return self.project_root / ".jarvis" / "c2rust" / "symbols.jsonl"
97
+
98
+ self.data_path = _resolve_data_path(Path(db_path))
99
+ if not self.data_path.exists():
100
+ raise FileNotFoundError(f"未找到 symbols.jsonl: {self.data_path}")
101
+ # Initialize in-memory graph structures
102
+ self.adj: Dict[int, List[str]] = {}
103
+ self.name_to_id: Dict[str, int] = {}
104
+ self.fn_by_id: Dict[int, _FnMeta] = {}
105
+ """
106
+ 从 symbols.jsonl 加载符号元数据与邻接关系(统一处理函数与类型,按 ref 构建名称邻接)。
107
+ """
108
+ rows_loaded = 0
109
+ try:
110
+ with open(self.data_path, "r", encoding="utf-8") as f:
111
+ for line in f:
112
+ line = line.strip()
113
+ if not line:
114
+ continue
115
+ try:
116
+ obj = json.loads(line)
117
+ except Exception:
118
+ continue
119
+ # 不区分函数与类型,统一处理 symbols.jsonl 中的所有记录
120
+ rows_loaded += 1
121
+ fid = int(obj.get("id") or rows_loaded)
122
+ nm = obj.get("name") or ""
123
+ qn = obj.get("qualified_name") or ""
124
+ sg = obj.get("signature") or ""
125
+ fp = obj.get("file") or ""
126
+ refs = obj.get("ref")
127
+ # 不兼容旧数据:严格要求为列表类型,缺失则视为空
128
+ if not isinstance(refs, list):
129
+ refs = []
130
+ refs = [c for c in refs if isinstance(c, str) and c]
131
+ self.adj[fid] = refs
132
+ # 建立名称索引与函数元信息,供子图遍历与上下文构造使用
133
+ if isinstance(nm, str) and nm:
134
+ self.name_to_id.setdefault(nm, fid)
135
+ if isinstance(qn, str) and qn:
136
+ self.name_to_id.setdefault(qn, fid)
137
+ try:
138
+ rel_file = self._rel_path(fp)
139
+ except Exception:
140
+ rel_file = fp
141
+ self.fn_by_id[fid] = _FnMeta(
142
+ id=fid,
143
+ name=nm,
144
+ qname=qn,
145
+ signature=sg,
146
+ file=rel_file,
147
+ refs=refs,
148
+ )
149
+ except FileNotFoundError:
150
+ raise
151
+ except Exception:
152
+ # 保持健壮性:出错时保持加载器为空
153
+ pass
154
+
155
+ def _rel_path(self, abs_path: str) -> str:
156
+ try:
157
+ p = Path(abs_path).resolve()
158
+ return str(p.relative_to(self.project_root))
159
+ except Exception:
160
+ return abs_path
161
+
162
+ def collect_subgraph(self, root_id: int) -> Tuple[Set[int], Set[str]]:
163
+ """
164
+ 从 root_id 出发,收集所有可达的内部函数 (visited_ids) 与外部调用名称 (externals)
165
+ """
166
+ visited: Set[int] = set()
167
+ externals: Set[str] = set()
168
+ stack: List[int] = [root_id]
169
+ visited.add(root_id)
170
+ while stack:
171
+ src = stack.pop()
172
+ for callee in self.adj.get(src, []):
173
+ cid = self.name_to_id.get(callee)
174
+ if cid is not None:
175
+ if cid not in visited:
176
+ visited.add(cid)
177
+ stack.append(cid)
178
+ else:
179
+ externals.add(callee)
180
+ return visited, externals
181
+
182
+ def build_roots_context(
183
+ self,
184
+ roots: List[int],
185
+ max_functions_per_ns: int = 200,
186
+ max_namespaces_per_root: int = 50,
187
+ ) -> List[Dict[str, Any]]:
188
+ """
189
+ 为每个根函数构造上下文(仅函数名的调用关系,且不包含任何其他信息):
190
+ - root_function: 根函数的简单名称(不包含签名/限定名)
191
+ - functions: 该根函数子图内所有可达函数的简单名称列表(不包含签名/限定名),去重、排序、可选截断
192
+ 注意:
193
+ - 不包含文件路径、签名、限定名、命名空间、外部符号等任何其他元信息
194
+ """
195
+ root_contexts: List[Dict[str, Any]] = []
196
+ for rid in roots:
197
+ meta = self.fn_by_id.get(rid)
198
+ root_label = (meta.name or f"fn_{rid}") if meta else f"fn_{rid}"
199
+
200
+ visited_ids, _externals = self.collect_subgraph(rid)
201
+ # 收集所有简单函数名
202
+ fn_names: List[str] = []
203
+ for fid in sorted(visited_ids):
204
+ m = self.fn_by_id.get(fid)
205
+ if not m:
206
+ continue
207
+ simple = m.name or f"fn_{fid}"
208
+ fn_names.append(simple)
209
+
210
+ # 去重并排序
211
+ try:
212
+ fn_names = sorted(list(dict.fromkeys(fn_names)))
213
+ except Exception:
214
+ fn_names = sorted(list(set(fn_names)))
215
+
216
+ root_contexts.append(
217
+ {
218
+ "root_function": root_label,
219
+ "functions": fn_names,
220
+ }
221
+ )
222
+ return root_contexts
223
+
224
+
225
+ def _perform_pre_cleanup_for_planner(project_root: Union[Path, str]) -> None:
226
+ """
227
+ 预清理:如存在将删除将要生成的 crate 目录、当前目录的 workspace 文件 Cargo.toml、
228
+ 以及 project_root/.jarvis/c2rust 下的 progress.json 与 symbol_map.jsonl。
229
+ 用户不同意则直接退出。
230
+ """
231
+ try:
232
+ import sys
233
+ import shutil
234
+ cwd = Path(".").resolve()
235
+ try:
236
+ requested_root = Path(project_root).resolve()
237
+ except Exception:
238
+ requested_root = Path(project_root)
239
+ created_dir = cwd.parent / f"{cwd.name}_rs" if requested_root == cwd else requested_root
240
+
241
+ cargo_path = cwd / "Cargo.toml"
242
+ data_dir = requested_root / ".jarvis" / "c2rust"
243
+ progress_path = data_dir / "progress.json"
244
+ symbol_map_jsonl_path = data_dir / "symbol_map.jsonl"
245
+
246
+ targets: List[str] = []
247
+ if created_dir.exists():
248
+ targets.append(f"- 删除 crate 目录(如存在):{created_dir}")
249
+ if cargo_path.exists():
250
+ targets.append(f"- 删除工作区文件:{cargo_path}")
251
+ if progress_path.exists():
252
+ targets.append(f"- 删除进度文件:{progress_path}")
253
+ if symbol_map_jsonl_path.exists():
254
+ targets.append(f"- 删除符号映射文件:{symbol_map_jsonl_path}")
255
+
256
+ if not targets:
257
+ return
258
+
259
+ tip_lines = ["将执行以下清理操作:"] + targets + ["", "是否继续?"]
260
+ if not user_confirm("\n".join(tip_lines), default=False):
261
+ print("[c2rust-llm-planner] 用户取消清理操作,退出。")
262
+ sys.exit(0)
263
+
264
+ if created_dir.exists():
265
+ shutil.rmtree(created_dir, ignore_errors=True)
266
+ if cargo_path.exists():
267
+ cargo_path.unlink()
268
+ if progress_path.exists():
269
+ progress_path.unlink()
270
+ if symbol_map_jsonl_path.exists():
271
+ symbol_map_jsonl_path.unlink()
272
+ except Exception:
273
+ pass
274
+
275
+ def _resolve_created_dir(target_root: Union[Path, str]) -> Path:
276
+ """
277
+ 解析 crate 目录路径:
278
+ - 若 target_root 为 "." 或解析后等于当前工作目录,则返回 "<cwd.name>_rs" 目录;
279
+ - 否则返回解析后的目标路径;
280
+ - 解析失败则回退到 Path(target_root)。
281
+ """
282
+ try:
283
+ cwd = Path(".").resolve()
284
+ try:
285
+ resolved_target = Path(target_root).resolve()
286
+ except Exception:
287
+ resolved_target = Path(target_root)
288
+ if target_root == "." or resolved_target == cwd:
289
+ return cwd.parent / f"{cwd.name}_rs"
290
+ return resolved_target
291
+ except Exception:
292
+ return Path(target_root)
293
+
294
+ class LLMRustCratePlannerAgent:
295
+ """
296
+ 使用 jarvis_agent.Agent 调用 LLM 来生成 Rust crate 规划(YAML)。
297
+ """
298
+
299
+ def __init__(
300
+ self,
301
+ project_root: Union[Path, str] = ".",
302
+ db_path: Optional[Union[Path, str]] = None,
303
+ llm_group: Optional[str] = None,
304
+ ):
305
+ self.project_root = Path(project_root).resolve()
306
+ self.db_path = (
307
+ Path(db_path).resolve()
308
+ if db_path is not None
309
+ else (self.project_root / ".jarvis" / "c2rust" / "symbols.jsonl")
310
+ )
311
+ self.llm_group = llm_group
312
+ self.loader = _GraphLoader(self.db_path, self.project_root)
313
+
314
+ def _crate_name(self) -> str:
315
+ """
316
+ 计算crate名称:
317
+ - 当 project_root 为当前目录时,返回 "<当前目录名>_rs"
318
+ - 否则返回 project_root 的目录名
319
+ - 输出用于命名/提示,保持下划线风格(不影响 Cargo 包名)
320
+ """
321
+ try:
322
+ cwd = Path(".").resolve()
323
+ if self.project_root.resolve() == cwd:
324
+ base = f"{cwd.name}_rs"
325
+ else:
326
+ base = self.project_root.name or "c2rust_crate"
327
+ except Exception:
328
+ base = "c2rust_crate"
329
+ return _sanitize_mod_name(base)
330
+
331
+ def _has_original_main(self) -> bool:
332
+ """
333
+ 判断原始项目是否包含 main 函数:
334
+ - 若 symbols 图谱中存在函数名为 'main' 或限定名以 '::main' 结尾,则认为存在
335
+ """
336
+ try:
337
+ for m in self.loader.fn_by_id.values():
338
+ n = (m.name or "").strip()
339
+ q = (m.qname or "").strip()
340
+ if n == "main" or q.endswith("::main"):
341
+ return True
342
+ except Exception:
343
+ pass
344
+ return False
345
+
346
+ def _order_path(self) -> Path:
347
+ """
348
+ 返回 translation_order.jsonl 的标准路径:<project_root>/.jarvis/c2rust/translation_order.jsonl
349
+ """
350
+ return self.project_root / ".jarvis" / "c2rust" / "translation_order.jsonl"
351
+
352
+ def _build_roots_context_from_order(self) -> List[Dict[str, Any]]:
353
+ """
354
+ 基于 translation_order.jsonl 生成用于规划的上下文:
355
+ - 以每个 step 的 roots 标签为分组键(通常每步一个 root 标签)
356
+ - 函数列表来自每步的 items 中的符号 'name' 字段,按 root 聚合去重
357
+ - 跳过无 roots 标签的 residual 步骤(仅保留明确 root 的上下文)
358
+ - 若最终未收集到任何 root 组,则回退为单组 'project',包含所有 items 的函数名集合
359
+ """
360
+ import json
361
+ order_path = self._order_path()
362
+ if not order_path.exists():
363
+ raise FileNotFoundError(f"未找到 translation_order.jsonl: {order_path}")
364
+
365
+ groups: Dict[str, List[str]] = {}
366
+ with order_path.open("r", encoding="utf-8") as f:
367
+ for line in f:
368
+ line = line.strip()
369
+ if not line:
370
+ continue
371
+ try:
372
+ obj = json.loads(line)
373
+ except Exception:
374
+ continue
375
+ roots = obj.get("roots") or []
376
+ items = obj.get("items") or []
377
+ if not isinstance(items, list) or not items:
378
+ continue
379
+ root_labels = [str(r).strip() for r in roots if isinstance(r, str) and str(r).strip()]
380
+ if not root_labels:
381
+ continue
382
+ step_names: List[str] = []
383
+ for it in items:
384
+ if isinstance(it, dict):
385
+ nm = it.get("name") or ""
386
+ if isinstance(nm, str) and nm.strip():
387
+ step_names.append(str(nm).strip())
388
+ if not step_names:
389
+ continue
390
+ try:
391
+ step_names = list(dict.fromkeys(step_names))
392
+ except Exception:
393
+ step_names = sorted(list(set(step_names)))
394
+ for r in root_labels:
395
+ groups.setdefault(r, []).extend(step_names)
396
+
397
+ contexts: List[Dict[str, Any]] = []
398
+ for root_label, names in groups.items():
399
+ try:
400
+ names = list(dict.fromkeys(names))
401
+ except Exception:
402
+ names = sorted(list(set(names)))
403
+ contexts.append({"root_function": root_label, "functions": sorted(names)})
404
+
405
+ if not contexts:
406
+ all_names: List[str] = []
407
+ try:
408
+ with order_path.open("r", encoding="utf-8") as f:
409
+ for line in f:
410
+ line = line.strip()
411
+ if not line:
412
+ continue
413
+ try:
414
+ obj = json.loads(line)
415
+ except Exception:
416
+ continue
417
+ items = obj.get("items") or []
418
+ if not isinstance(items, list):
419
+ continue
420
+ for it in items:
421
+ if isinstance(it, dict):
422
+ nm = it.get("name") or ""
423
+ if isinstance(nm, str) and nm.strip():
424
+ all_names.append(str(nm).strip())
425
+ try:
426
+ all_names = list(dict.fromkeys(all_names))
427
+ except Exception:
428
+ all_names = sorted(list(set(all_names)))
429
+ if all_names:
430
+ contexts.append({"root_function": "project", "functions": sorted(all_names)})
431
+ except Exception:
432
+ pass
433
+
434
+ return contexts
435
+
436
+ def _build_user_prompt(self, roots_context: List[Dict[str, Any]]) -> str:
437
+ """
438
+ 主对话阶段:传入上下文,不给出输出要求,仅用于让模型获取信息并触发进入总结阶段。
439
+ 请模型仅输出 <!!!COMPLETE!!!> 以进入总结(summary)阶段。
440
+ """
441
+ crate_name = self._crate_name()
442
+ has_main = self._has_original_main()
443
+ created_dir = _resolve_created_dir(self.project_root)
444
+ context_json = json.dumps(
445
+ {"meta": {"crate_name": crate_name, "main_present": has_main, "crate_dir": str(created_dir)}, "roots": roots_context},
446
+ ensure_ascii=False,
447
+ indent=2,
448
+ )
449
+ return f"""
450
+ 下面提供了项目的调用图上下文(JSON),请先通读理解,不要输出任何规划或YAML内容:
451
+ <context>
452
+ {context_json}
453
+ </context>
454
+
455
+ 如果已准备好进入总结阶段以生成完整输出,请仅输出:<!!!COMPLETE!!!>
456
+ """.strip()
457
+
458
+ def _build_system_prompt(self) -> str:
459
+ """
460
+ 系统提示:描述如何基于依赖关系进行 crate 规划的原则(不涉及对话流程或输出方式)
461
+ """
462
+ crate_name = self._crate_name()
463
+ return (
464
+ "你是资深 Rust 架构师。任务:根据给定的函数级调用关系(仅包含 root_function 及其可达的函数名列表),为目标项目规划合理的 Rust crate 结构。\n"
465
+ "\n"
466
+ "规划原则:\n"
467
+ "- 根导向:以每个 root_function 为边界组织顶层模块,形成清晰的入口与责任范围。\n"
468
+ "- 内聚优先:按调用内聚性拆分子模块,使强相关函数位于同一子模块,减少跨模块耦合。\n"
469
+ "- 去环与分层:尽量消除循环依赖;遵循由上到下的调用方向,保持稳定依赖方向与层次清晰。\n"
470
+ "- 共享抽取:被多个 root 使用的通用能力抽取到 common/ 或 shared/ 模块,避免重复与交叉依赖。\n"
471
+ "- 边界隔离:将平台/IO/外设等边界能力独立到 adapter/ 或 ffi/ 等模块(如存在)。\n"
472
+ "- 命名规范:目录/文件采用小写下划线;模块名简洁可读,避免特殊字符与过长名称。\n"
473
+ "- 可演进性:模块粒度适中,保留扩展点,便于后续重构与逐步替换遗留代码。\n"
474
+ "- 模块组织:每个目录的 mod.rs 声明其子目录与 .rs 子模块;顶层 lib.rs 汇聚导出主要模块与公共能力。\n"
475
+ "- 入口策略(务必遵循,bin 仅做入口,功能尽量在 lib 中实现):\n"
476
+ " * 若原始项目包含 main 函数:不要生成 src/main.rs;使用 src/bin/"
477
+ + crate_name
478
+ + ".rs 作为唯一可执行入口,并在其中仅保留最小入口逻辑(调用库层);共享代码放在 src/lib.rs;\n"
479
+ " * 若原始项目不包含 main 函数:不要生成任何二进制入口(不创建 src/main.rs 或 src/bin/),仅生成 src/lib.rs;\n"
480
+ " * 多可执行仅在确有多个清晰入口时才使用 src/bin/<name>.rs;每个 bin 文件仅做入口,尽量调用库;\n"
481
+ " * 二进制命名:<name> 使用小写下划线,体现入口意图,避免与模块/文件重名。\n"
482
+ )
483
+
484
+ def _build_summary_prompt(self, roots_context: List[Dict[str, Any]]) -> str:
485
+ """
486
+ 总结阶段:只输出目录结构的 YAML。
487
+ 要求:
488
+ - 仅输出一个 <PROJECT> 块
489
+ - <PROJECT> 与 </PROJECT> 之间必须是可解析的 YAML 列表,使用两空格缩进
490
+ - 目录以 '目录名/' 表示,子项为列表;文件为纯字符串
491
+ - 块外不得有任何字符(包括空行、注释、Markdown、解释文字、schema等)
492
+ - 不要输出 crate 名称或其他多余字段
493
+ """
494
+ has_main = self._has_original_main()
495
+ crate_name = self._crate_name()
496
+ guidance_common = """
497
+ 输出规范:
498
+ - 只输出一个 <PROJECT> 块
499
+ - 块外不得有任何字符(包括空行、注释、Markdown 等)
500
+ - 块内必须是 YAML 列表:
501
+ - 目录项使用 '<name>/' 作为键,并在后面加冒号 ':',其值为子项列表
502
+ - 文件为字符串项(例如 'lib.rs')
503
+ - 不要创建与入口无关的占位文件
504
+ """.strip()
505
+ if has_main:
506
+ entry_rule = f"""
507
+ 入口约定(基于原始项目存在 main):
508
+ - 必须包含 src/lib.rs;
509
+ - 不要包含 src/main.rs;
510
+ - 必须包含 src/bin/{crate_name}.rs,作为唯一可执行入口(仅做入口,调用库逻辑);
511
+ - 如无明确多个入口,不要创建额外 bin 文件。
512
+ 正确示例(标准 YAML,带冒号):
513
+ <PROJECT>
514
+ - Cargo.toml
515
+ - src/:
516
+ - lib.rs
517
+ - bin/:
518
+ - {crate_name}.rs
519
+ </PROJECT>
520
+ """.strip()
521
+ else:
522
+ entry_rule = """
523
+ 入口约定(基于原始项目不存在 main):
524
+ - 必须包含 src/lib.rs;
525
+ - 不要包含 src/main.rs;
526
+ - 不要包含 src/bin/ 目录。
527
+ 正确示例(标准 YAML,带冒号):
528
+ <PROJECT>
529
+ - Cargo.toml
530
+ - src/:
531
+ - lib.rs
532
+ </PROJECT>
533
+ """.strip()
534
+ guidance = f"{guidance_common}\n{entry_rule}"
535
+ return f"""
536
+ 请基于之前对话中已提供的<context>信息,生成总结输出(项目目录结构的 YAML)。严格遵循以下要求:
537
+
538
+ {guidance}
539
+
540
+ 你的输出必须仅包含以下单个块(用项目的真实目录结构替换块内内容):
541
+ <PROJECT>
542
+ - ...
543
+ </PROJECT>
544
+ """.strip()
545
+
546
+ def _extract_yaml_from_project(self, text: str) -> str:
547
+ """
548
+ 从 <PROJECT> 块中提取内容作为最终 YAML;若未匹配,返回原文本(兜底)。
549
+ """
550
+ if not isinstance(text, str) or not text:
551
+ return ""
552
+ m_proj = re.search(r"<PROJECT>([\s\S]*?)</PROJECT>", text, flags=re.IGNORECASE)
553
+ if m_proj:
554
+ return m_proj.group(1).strip()
555
+ return text.strip()
556
+
557
+ def _validate_project_entries(self, entries: List[Any]) -> Tuple[bool, str]:
558
+ """
559
+ 校验目录结构是否满足强约束:
560
+ - 必须存在 src/lib.rs
561
+ - 若原始项目包含 main:
562
+ * 不允许 src/main.rs
563
+ * 必须包含 src/bin/<crate_name>.rs
564
+ - 若原始项目不包含 main:
565
+ * 不允许 src/main.rs
566
+ * 不允许存在 src/bin/ 目录
567
+ 返回 (是否通过, 错误原因)
568
+ """
569
+ if not isinstance(entries, list) or not entries:
570
+ return False, "YAML 不可解析或为空列表"
571
+
572
+ # 提取 src 目录子项
573
+ src_children: Optional[List[Any]] = None
574
+ for it in entries:
575
+ if isinstance(it, dict) and len(it) == 1:
576
+ k, v = next(iter(it.items()))
577
+ kk = str(k).rstrip("/").strip().lower()
578
+ if kk == "src":
579
+ if isinstance(v, list):
580
+ src_children = v
581
+ else:
582
+ src_children = []
583
+ break
584
+ if src_children is None:
585
+ return False, "缺少 src 目录"
586
+
587
+ # 建立便捷索引
588
+ def has_file(name: str) -> bool:
589
+ for ch in src_children or []:
590
+ if isinstance(ch, str) and ch.strip().lower() == name.lower():
591
+ return True
592
+ return False
593
+
594
+ def find_dir(name: str) -> Optional[List[Any]]:
595
+ for ch in src_children or []:
596
+ if isinstance(ch, dict) and len(ch) == 1:
597
+ k, v = next(iter(ch.items()))
598
+ kk = str(k).rstrip("/").strip().lower()
599
+ if kk == name.lower():
600
+ return v if isinstance(v, list) else []
601
+ return None
602
+
603
+ # 1) 必须包含 lib.rs
604
+ if not has_file("lib.rs"):
605
+ return False, "src 目录下必须包含 lib.rs"
606
+
607
+ has_main = self._has_original_main()
608
+ crate_name = self._crate_name()
609
+
610
+ # 2) 入口约束
611
+ if has_main:
612
+ # 不允许 src/main.rs
613
+ if has_file("main.rs"):
614
+ return False, "原始项目包含 main:不应生成 src/main.rs,请使用 src/bin/<crate>.rs"
615
+ # 必须包含 src/bin/<crate_name>.rs
616
+ bin_children = find_dir("bin")
617
+ if bin_children is None:
618
+ return False, f"原始项目包含 main:必须包含 src/bin/{crate_name}.rs"
619
+ expect_bin = f"{crate_name}.rs".lower()
620
+ if not any(isinstance(ch, str) and ch.strip().lower() == expect_bin for ch in bin_children):
621
+ return False, f"原始项目包含 main:必须包含 src/bin/{crate_name}.rs"
622
+ else:
623
+ # 不允许 src/main.rs
624
+ if has_file("main.rs"):
625
+ return False, "原始项目不包含 main:不应生成 src/main.rs"
626
+ # 不允许有 bin 目录
627
+ if find_dir("bin") is not None:
628
+ return False, "原始项目不包含 main:不应生成 src/bin/ 目录"
629
+
630
+ return True, ""
631
+
632
+ def _build_retry_summary_prompt(self, base_summary_prompt: str, error_reason: str) -> str:
633
+ """
634
+ 在原始 summary_prompt 基础上,附加错误反馈,要求严格重试。
635
+ """
636
+ feedback = (
637
+ "\n\n[格式校验失败,必须重试]\n"
638
+ f"- 失败原因:{error_reason}\n"
639
+ "- 请严格遵循上述“输出规范”与“入口约定”,重新输出;\n"
640
+ "- 仅输出一个 <PROJECT> 块,块内为可解析的 YAML 列表;块外不得有任何字符。\n"
641
+ )
642
+ return base_summary_prompt + feedback
643
+
644
+ def _get_project_yaml_text(self) -> str:
645
+ """
646
+ 执行主流程并返回原始 <PROJECT> YAML 文本,不进行解析。
647
+ 若格式校验失败,将自动重试,直到满足为止(不设置最大重试次数)。
648
+ """
649
+ # 从 translation_order.jsonl 生成上下文,不再基于 symbols.jsonl 的调用图遍历
650
+ roots_ctx = self._build_roots_context_from_order()
651
+
652
+ system_prompt = self._build_system_prompt()
653
+ user_prompt = self._build_user_prompt(roots_ctx)
654
+ base_summary_prompt = self._build_summary_prompt(roots_ctx)
655
+
656
+ last_yaml_text = ""
657
+ last_error = "未知错误"
658
+ attempt = 0
659
+ while True:
660
+ attempt += 1
661
+ # 首次使用基础 summary_prompt;失败后附加反馈
662
+ summary_prompt = (
663
+ base_summary_prompt if attempt == 1 else self._build_retry_summary_prompt(base_summary_prompt, last_error)
664
+ )
665
+
666
+ agent = Agent(
667
+ system_prompt=system_prompt,
668
+ name="C2Rust-LLM-Module-Planner",
669
+ model_group=self.llm_group,
670
+ summary_prompt=summary_prompt,
671
+ need_summary=True,
672
+ auto_complete=True,
673
+ use_tools=["execute_script", "read_code", "retrieve_memory", "save_memory"],
674
+ plan=False, # 关闭内置任务规划
675
+ non_interactive=True, # 非交互
676
+ use_methodology=False,
677
+ use_analysis=False,
678
+ )
679
+
680
+ # 进入主循环:第一轮仅输出 <!!!COMPLETE!!!> 触发自动完成;随后 summary 输出 <PROJECT> 块(仅含 YAML)
681
+ summary_output = agent.run(user_prompt) # type: ignore
682
+ project_text = str(summary_output) if summary_output is not None else ""
683
+ yaml_text = self._extract_yaml_from_project(project_text)
684
+ last_yaml_text = yaml_text
685
+
686
+ # 尝试解析并校验
687
+ try:
688
+ entries = _parse_project_yaml_entries(yaml_text)
689
+ except Exception:
690
+ entries = []
691
+
692
+ ok, reason = self._validate_project_entries(entries)
693
+ if ok:
694
+ return yaml_text
695
+ else:
696
+ last_error = reason
697
+
698
+ def plan_crate_yaml_with_project(self) -> List[Any]:
699
+ """
700
+ 执行主流程并返回解析后的 YAML 对象(列表):
701
+ - 列表项:
702
+ * 字符串:文件,如 "lib.rs"
703
+ * 字典:目录及其子项,如 {"src": [ ... ]}
704
+ """
705
+ yaml_text = self._get_project_yaml_text()
706
+ yaml_entries = _parse_project_yaml_entries(yaml_text)
707
+ return yaml_entries
708
+
709
+ def plan_crate_yaml_text(self) -> str:
710
+ """
711
+ 执行主流程但返回原始 <PROJECT> YAML 文本,不进行解析。
712
+ 便于后续按原样应用目录结构,避免早期解析失败导致信息丢失。
713
+ """
714
+ return self._get_project_yaml_text()
715
+ """
716
+ 执行主流程但返回原始 <PROJECT> YAML 文本,不进行解析。
717
+ 便于后续按原样应用目录结构,避免早期解析失败导致信息丢失。
718
+ """
719
+ return self._get_project_yaml_text()
720
+
721
+
722
+ def plan_crate_yaml_text(
723
+ project_root: Union[Path, str] = ".",
724
+ db_path: Optional[Union[Path, str]] = None,
725
+ llm_group: Optional[str] = None,
726
+ skip_cleanup: bool = False,
727
+ ) -> str:
728
+ """
729
+ 返回 LLM 生成的目录结构原始 YAML 文本(来自 <PROJECT> 块)。
730
+ 在规划前执行预清理并征询用户确认:删除将要生成的 crate 目录、当前目录的 Cargo.toml 工作区文件,以及 .jarvis/c2rust 下的 progress.json 与 symbol_map.jsonl。
731
+ 用户不同意则退出程序。
732
+ 当 skip_cleanup=True 时,跳过清理与确认(用于外层已处理的场景)。
733
+ """
734
+ # 若外层已处理清理确认,则跳过本函数的清理与确认(避免重复询问)
735
+ if skip_cleanup:
736
+ agent = LLMRustCratePlannerAgent(project_root=project_root, db_path=db_path, llm_group=llm_group)
737
+ return agent.plan_crate_yaml_text()
738
+
739
+ _perform_pre_cleanup_for_planner(project_root)
740
+
741
+ agent = LLMRustCratePlannerAgent(project_root=project_root, db_path=db_path, llm_group=llm_group)
742
+ return agent.plan_crate_yaml_text()
743
+
744
+
745
+ def plan_crate_yaml_llm(
746
+ project_root: Union[Path, str] = ".",
747
+ db_path: Optional[Union[Path, str]] = None,
748
+ skip_cleanup: bool = False,
749
+ ) -> List[Any]:
750
+ """
751
+ 便捷函数:使用 LLM 生成 Rust crate 模块规划(解析后的对象)。
752
+ 在规划前执行预清理并征询用户确认:删除将要生成的 crate 目录、当前目录的 Cargo.toml 工作区文件,以及 .jarvis/c2rust 下的 progress.json 与 symbol_map.jsonl。
753
+ 用户不同意则退出程序。
754
+ 当 skip_cleanup=True 时,跳过清理与确认(用于外层已处理的场景)。
755
+ """
756
+ # 若外层已处理清理确认,则跳过本函数的清理与确认(避免重复询问)
757
+ if skip_cleanup:
758
+ agent = LLMRustCratePlannerAgent(project_root=project_root, db_path=db_path)
759
+ return agent.plan_crate_yaml_with_project()
760
+
761
+ _perform_pre_cleanup_for_planner(project_root)
762
+
763
+ agent = LLMRustCratePlannerAgent(project_root=project_root, db_path=db_path)
764
+ return agent.plan_crate_yaml_with_project()
765
+
766
+
767
+ def entries_to_yaml(entries: List[Any]) -> str:
768
+ """
769
+ 将解析后的 entries 列表序列化为 YAML 文本(目录使用 'name/:' 形式,文件为字符串)
770
+ """
771
+ def _entries_to_yaml(items, indent=0):
772
+ lines: List[str] = []
773
+ for it in (items or []):
774
+ if isinstance(it, str):
775
+ lines.append(" " * indent + f"- {it}")
776
+ elif isinstance(it, dict) and len(it) == 1:
777
+ name, children = next(iter(it.items()))
778
+ name = str(name).rstrip("/")
779
+ lines.append(" " * indent + f"- {name}/:")
780
+ lines.extend(_entries_to_yaml(children or [], indent + 1))
781
+ return lines
782
+
783
+ return "\n".join(_entries_to_yaml(entries))
784
+
785
+
786
+ def _parse_project_yaml_entries_fallback(yaml_text: str) -> List[Any]:
787
+ """
788
+ Fallback 解析器:当 PyYAML 不可用或解析失败时,按约定的缩进/列表语法解析 <PROJECT> 块。
789
+ 支持的子集:
790
+ - 列表项以 "- " 开头
791
+ - 目录项以 "- <name>/:", 其子项为下一层缩进(+2 空格)的列表
792
+ - 文件项为 "- <filename>"
793
+ """
794
+ def leading_spaces(s: str) -> int:
795
+ return len(s) - len(s.lstrip(" "))
796
+
797
+ lines = [ln.rstrip() for ln in str(yaml_text or "").splitlines()]
798
+ idx = 0
799
+ n = len(lines)
800
+
801
+ # 跳过非列表起始行
802
+ while idx < n and not lines[idx].lstrip().startswith("- "):
803
+ idx += 1
804
+
805
+ def parse_list(expected_indent: int) -> List[Any]:
806
+ nonlocal idx
807
+ items: List[Any] = []
808
+ while idx < n:
809
+ line = lines[idx]
810
+ if not line.strip():
811
+ idx += 1
812
+ continue
813
+ indent = leading_spaces(line)
814
+ if indent < expected_indent:
815
+ break
816
+ if not line.lstrip().startswith("- "):
817
+ break
818
+
819
+ # 去掉 "- "
820
+ content = line[indent + 2 :].strip()
821
+
822
+ # 目录项:以 ":" 结尾(形如 "src/:")
823
+ if content.endswith(":"):
824
+ key = content[:-1].strip()
825
+ idx += 1 # 消费当前目录行
826
+ children = parse_list(expected_indent + 2)
827
+ # 规范化目录键为以 "/" 结尾(apply 时会 rstrip("/"),二者均可)
828
+ if not str(key).endswith("/"):
829
+ key = f"{str(key).rstrip('/')}/"
830
+ items.append({key: children})
831
+ else:
832
+ # 文件项
833
+ items.append(content)
834
+ idx += 1
835
+ return items
836
+
837
+ base_indent = leading_spaces(lines[idx]) if idx < n else 0
838
+ return parse_list(base_indent)
839
+
840
+
841
+ def _parse_project_yaml_entries(yaml_text: str) -> List[Any]:
842
+ """
843
+ 使用 PyYAML 解析 <PROJECT> 块中的目录结构 YAML 为列表结构:
844
+ - 文件项: 字符串,如 "lib.rs"
845
+ - 目录项: 字典,形如 {"src/": [ ... ]} 或 {"src": [ ... ]}
846
+ 优先使用 PyYAML;若不可用或解析失败,则回退到轻量解析器以最大化兼容性。
847
+ """
848
+ try:
849
+ import yaml # type: ignore
850
+ data = yaml.safe_load(yaml_text)
851
+ if isinstance(data, list):
852
+ return data
853
+ except Exception:
854
+ pass
855
+ # 回退
856
+ return _parse_project_yaml_entries_fallback(yaml_text)
857
+
858
+
859
+ def _ensure_pub_mod_declarations(existing_text: str, child_mods: List[str]) -> str:
860
+ """
861
+ 在给定文本中确保存在并升级子模块声明为 `pub mod <name>;`:
862
+ - 解析已有的 `mod`/`pub mod`/`pub(...) mod` 声明;
863
+ - 已存在但非 pub 的同名声明就地升级为 `pub mod`,保留原行的缩进;
864
+ - 不存在的模块名则在末尾追加一行 `pub mod <name>;`;
865
+ - 返回更新后的完整文本(保留结尾换行)。
866
+ """
867
+ try:
868
+ lines = (existing_text or "").splitlines()
869
+ except Exception:
870
+ lines = []
871
+ mod_decl_pattern = re.compile(r'^\s*(pub(?:\s*\([^)]+\))?\s+)?mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*;\s*$')
872
+ name_to_indices: Dict[str, List[int]] = {}
873
+ name_has_pub: Set[str] = set()
874
+ for i, ln in enumerate(lines):
875
+ m = mod_decl_pattern.match(ln.strip())
876
+ if not m:
877
+ continue
878
+ mod_name = m.group(2)
879
+ name_to_indices.setdefault(mod_name, []).append(i)
880
+ if m.group(1):
881
+ name_has_pub.add(mod_name)
882
+ for mod_name in sorted(set(child_mods or [])):
883
+ if mod_name in name_to_indices:
884
+ if mod_name not in name_has_pub:
885
+ for idx in name_to_indices[mod_name]:
886
+ ws_match = re.match(r'^(\s*)', lines[idx])
887
+ leading_ws = ws_match.group(1) if ws_match else ""
888
+ lines[idx] = f"{leading_ws}pub mod {mod_name};"
889
+ else:
890
+ lines.append(f"pub mod {mod_name};")
891
+ return "\n".join(lines).rstrip() + ("\n" if lines else "")
892
+
893
+ def _apply_entries_with_mods(entries: List[Any], base_path: Path) -> None:
894
+ """
895
+ 根据解析出的 entries 创建目录与文件结构(不在此阶段写入/更新任何 Rust 源文件内容):
896
+ - 对于目录项:创建目录,并递归创建其子项;
897
+ - 对于文件项:若不存在则创建空文件;
898
+ 约束与约定:
899
+ - crate 根的 src 目录:不生成 src/mod.rs,也不写入 src/lib.rs 的模块声明;
900
+ - 非 src 目录:不创建或更新 mod.rs;如需创建 mod.rs,请在 YAML 中显式列出;
901
+ - 模块声明的补齐将在后续 CodeAgent 阶段完成(扫描目录结构并最小化补齐 pub mod 声明)。
902
+ """
903
+ def apply_item(item: Any, dir_path: Path) -> None:
904
+ if isinstance(item, str):
905
+ # 文件
906
+ file_path = dir_path / item
907
+ file_path.parent.mkdir(parents=True, exist_ok=True)
908
+ if not file_path.exists():
909
+ try:
910
+ file_path.touch(exist_ok=True)
911
+ except Exception:
912
+ pass
913
+ return
914
+
915
+ if isinstance(item, dict) and len(item) == 1:
916
+ dir_name, children = next(iter(item.items()))
917
+ name = str(dir_name).rstrip("/").strip()
918
+ new_dir = dir_path / name
919
+ new_dir.mkdir(parents=True, exist_ok=True)
920
+
921
+ child_mods: List[str] = []
922
+ mod_rs_present = False
923
+ # 是否为 crate 根下的 src 目录
924
+ is_src_root_dir = (new_dir == base_path / "src")
925
+
926
+ # 先创建子项
927
+ for child in (children or []):
928
+ if isinstance(child, str):
929
+ apply_item(child, new_dir)
930
+ # 收集 .rs 文件作为子模块
931
+ if child.endswith(".rs") and child != "mod.rs":
932
+ stem = Path(child).stem
933
+ # 在 src 根目录下,忽略 lib.rs 与 main.rs 的自引用
934
+ if is_src_root_dir and stem in ("lib", "main"):
935
+ pass
936
+ else:
937
+ child_mods.append(stem)
938
+ if child == "mod.rs":
939
+ mod_rs_present = True
940
+ elif isinstance(child, dict):
941
+ # 子目录
942
+ sub_name = list(child.keys())[0]
943
+ sub_mod_name = str(sub_name).rstrip("/").strip()
944
+ child_mods.append(sub_mod_name)
945
+ apply_item(child, new_dir)
946
+
947
+ # 对 crate 根的 src 目录,使用 lib.rs 聚合子模块,不创建/更新 src/mod.rs
948
+ if is_src_root_dir:
949
+ # 不在 src 根目录写入任何文件内容;仅由子项创建对应空文件(如有)
950
+ return
951
+
952
+ # 非 src 目录:
953
+ # 为避免覆盖现有实现,当前阶段不创建或更新 mod.rs 内容。
954
+ # 如需创建 mod.rs,应在 YAML 中显式指定为文件项;
955
+ # 如需补齐模块声明,将由后续的 CodeAgent 阶段根据目录结构自动补齐。
956
+ return
957
+
958
+ for entry in entries:
959
+ apply_item(entry, base_path)
960
+
961
+
962
+ def _ensure_cargo_toml(base_dir: Path, package_name: str) -> None:
963
+ """
964
+ 确保在 base_dir 下存在合理的 Cargo.toml:
965
+ - 如果不存在,则创建最小可用的 Cargo.toml,并设置 package.name = package_name
966
+ - 如果已存在,则不覆盖现有内容(避免误改)
967
+ """
968
+ cargo_path = base_dir / "Cargo.toml"
969
+ if cargo_path.exists():
970
+ return
971
+ try:
972
+ cargo_path.touch(exist_ok=True)
973
+ except Exception:
974
+ pass
975
+
976
+
977
+ def apply_project_structure_from_yaml(yaml_text: str, project_root: Union[Path, str] = ".") -> None:
978
+ """
979
+ 基于 Agent 返回的 <PROJECT> 中的目录结构 YAML,创建实际目录与文件(不在此阶段写入或更新任何 Rust 源文件内容)。
980
+ - project_root: 目标应用路径;当为 "."(默认)时,将使用“父目录/当前目录名_rs”作为crate根目录
981
+ 注意:模块声明(mod/pub mod)补齐将在后续的 CodeAgent 步骤中完成。按新策略不再创建或更新 workspace(构建直接在 crate 目录内进行)。
982
+ """
983
+ entries = _parse_project_yaml_entries(yaml_text)
984
+ if not entries:
985
+ # 严格模式:解析失败直接报错并退出,由上层 CLI 捕获打印错误
986
+ raise ValueError("[c2rust-llm-planner] 从LLM输出解析目录结构失败。正在中止。")
987
+ requested_root = Path(project_root).resolve()
988
+ try:
989
+ cwd = Path(".").resolve()
990
+ if requested_root == cwd:
991
+ # 默认crate不能设置为 .,设置为 父目录/当前目录名_rs(与当前目录同级)
992
+ base_dir = cwd.parent / f"{cwd.name}_rs"
993
+ else:
994
+ base_dir = requested_root
995
+ except Exception:
996
+ base_dir = requested_root
997
+ base_dir.mkdir(parents=True, exist_ok=True)
998
+ # crate name 与目录名保持一致(用于 Cargo 包名,允许连字符)
999
+ crate_pkg_name = base_dir.name
1000
+ _apply_entries_with_mods(entries, base_dir)
1001
+ # 确保 Cargo.toml 存在并设置包名
1002
+ _ensure_cargo_toml(base_dir, crate_pkg_name)
1003
+
1004
+ # 在当前工作目录创建/更新 workspace,使该 crate 作为成员
1005
+ def _ensure_workspace_member(root_dir: Path, member_path: Path) -> None:
1006
+ """
1007
+ 在 root_dir 下的 Cargo.toml 中确保 [workspace] 成员包含 member_path(相对于 root_dir 的路径)。
1008
+ - 若 Cargo.toml 不存在:创建最小可用的 workspace 文件;
1009
+ - 若存在但无 [workspace]:追加 [workspace] 与 members;
1010
+ - 若存在且有 [workspace]:将成员路径加入 members(若不存在)。
1011
+ 尽量保留原有内容与格式,最小修改。
1012
+ """
1013
+ cargo_path = root_dir / "Cargo.toml"
1014
+ # 计算成员的相对路径
1015
+ try:
1016
+ rel_member = str(member_path.resolve().relative_to(root_dir.resolve()))
1017
+ except Exception:
1018
+ rel_member = member_path.name
1019
+
1020
+ if not cargo_path.exists():
1021
+ content = f"""[workspace]
1022
+ members = ["{rel_member}"]
1023
+ """
1024
+ try:
1025
+ cargo_path.write_text(content, encoding="utf-8")
1026
+ except Exception:
1027
+ pass
1028
+ return
1029
+
1030
+ try:
1031
+ txt = cargo_path.read_text(encoding="utf-8")
1032
+ except Exception:
1033
+ return
1034
+
1035
+ if "[workspace]" not in txt:
1036
+ new_txt = txt.rstrip() + f"\n\n[workspace]\nmembers = [\"{rel_member}\"]\n"
1037
+ try:
1038
+ cargo_path.write_text(new_txt, encoding="utf-8")
1039
+ except Exception:
1040
+ pass
1041
+ return
1042
+
1043
+ # 提取 workspace 区块(直到下一个表头或文件末尾)
1044
+ m_ws = re.search(r"(?s)(\[workspace\].*?)(?:\n\[|\Z)", txt)
1045
+ if not m_ws:
1046
+ new_txt = txt.rstrip() + f"\n\n[workspace]\nmembers = [\"{rel_member}\"]\n"
1047
+ try:
1048
+ cargo_path.write_text(new_txt, encoding="utf-8")
1049
+ except Exception:
1050
+ pass
1051
+ return
1052
+
1053
+ ws_block = m_ws.group(1)
1054
+
1055
+ # 查找 members 数组
1056
+ m_members = re.search(r"members\s*=\s*\[(.*?)\]", ws_block, flags=re.S)
1057
+ if not m_members:
1058
+ # 在 [workspace] 行后插入 members
1059
+ new_ws_block = re.sub(r"(\[workspace\]\s*)", r"\1\nmembers = [\"" + rel_member + "\"]\n", ws_block, count=1)
1060
+ else:
1061
+ inner = m_members.group(1)
1062
+ # 解析已有成员
1063
+ existing_vals = []
1064
+ for v in inner.split(","):
1065
+ vv = v.strip()
1066
+ if not vv:
1067
+ continue
1068
+ if vv.startswith('"') or vv.startswith("'"):
1069
+ vv = vv.strip('"').strip("'")
1070
+ existing_vals.append(vv)
1071
+ if rel_member in existing_vals:
1072
+ new_ws_block = ws_block # 已存在,不改动
1073
+ else:
1074
+ # 根据原格式选择分隔符
1075
+ sep = ", " if "\n" not in inner else ",\n"
1076
+ new_inner = inner.strip()
1077
+ if new_inner:
1078
+ new_inner = new_inner + f"{sep}\"{rel_member}\""
1079
+ else:
1080
+ new_inner = f"\"{rel_member}\""
1081
+ new_ws_block = ws_block[: m_members.start(1)] + new_inner + ws_block[m_members.end(1) :]
1082
+
1083
+ # 写回更新后的 workspace 区块
1084
+ new_txt = txt[: m_ws.start(1)] + new_ws_block + txt[m_ws.end(1) :]
1085
+ try:
1086
+ cargo_path.write_text(new_txt, encoding="utf-8")
1087
+ except Exception:
1088
+ pass
1089
+
1090
+ # 已弃用:不再将 crate 添加到 workspace(按新策略去除 workspace)
1091
+ # 构建与工具运行将直接在 crate 目录内进行
1092
+ pass
1093
+
1094
+
1095
+ def execute_llm_plan(
1096
+ out: Optional[Union[Path, str]] = None,
1097
+ apply: bool = False,
1098
+ crate_name: Optional[Union[Path, str]] = None,
1099
+ llm_group: Optional[str] = None,
1100
+ ) -> List[Any]:
1101
+ """
1102
+ 返回 LLM 生成的目录结构原始 YAML 文本(来自 <PROJECT> 块)。
1103
+ 不进行解析,便于后续按原样应用并在需要时使用更健壮的解析器处理。
1104
+ """
1105
+ # 预清理已由 plan_crate_yaml_text/plan_crate_yaml_llm 处理,此处不再重复确认与清理
1106
+ yaml_text = plan_crate_yaml_text(llm_group=llm_group)
1107
+ entries = _parse_project_yaml_entries(yaml_text)
1108
+ if not entries:
1109
+ raise ValueError("[c2rust-llm-planner] 从LLM输出解析目录结构失败。正在中止。")
1110
+
1111
+ # 2) 如需应用到磁盘
1112
+ if apply:
1113
+ target_root = crate_name if crate_name else "."
1114
+ try:
1115
+ apply_project_structure_from_yaml(yaml_text, project_root=target_root)
1116
+ print("[c2rust-llm-planner] 项目结构已应用。")
1117
+ except Exception as e:
1118
+ print(f"[c2rust-llm-planner] 应用项目结构失败: {e}")
1119
+ raise
1120
+
1121
+ # Post-apply: 检查生成的目录结构,使用 CodeAgent 更新 Cargo.toml
1122
+ from jarvis.jarvis_code_agent.code_agent import CodeAgent # 延迟导入以避免全局耦合
1123
+ import os
1124
+ import subprocess
1125
+
1126
+ # 解析 crate 目录路径(与 apply 逻辑保持一致)
1127
+ try:
1128
+ created_dir = _resolve_created_dir(target_root)
1129
+ except Exception:
1130
+ # 兜底:无法解析时直接使用传入的 target_root
1131
+ created_dir = Path(target_root)
1132
+
1133
+ # 在 crate 目录内执行 git 初始化与初始提交(按新策略)
1134
+ try:
1135
+ # 初始化 git 仓库(若已存在则该命令为幂等)
1136
+ subprocess.run(["git", "init"], check=False, cwd=str(created_dir))
1137
+ # 添加所有文件并尝试提交
1138
+ subprocess.run(["git", "add", "-A"], check=False, cwd=str(created_dir))
1139
+ subprocess.run(
1140
+ ["git", "commit", "-m", "[c2rust-llm-planner] init crate"],
1141
+ check=False,
1142
+ cwd=str(created_dir),
1143
+ )
1144
+ except Exception:
1145
+ # 保持稳健,不因 git 失败影响主流程
1146
+ pass
1147
+
1148
+ # 构建用于 CodeAgent 的目录上下文(简化版树形)
1149
+ def _format_tree(root: Path) -> str:
1150
+ lines: List[str] = []
1151
+ exclude = {".git", "target", ".jarvis"}
1152
+ if not root.exists():
1153
+ return ""
1154
+ for p in sorted(root.rglob("*")):
1155
+ if any(part in exclude for part in p.parts):
1156
+ continue
1157
+ rel = p.relative_to(root)
1158
+ depth = len(rel.parts) - 1
1159
+ indent = " " * depth
1160
+ name = rel.name + ("/" if p.is_dir() else "")
1161
+ lines.append(f"{indent}- {name}")
1162
+ return "\n".join(lines)
1163
+
1164
+ dir_ctx = _format_tree(created_dir)
1165
+ crate_pkg_name = created_dir.name
1166
+
1167
+ requirement_lines = [
1168
+ "目标:在该 crate 目录下确保 `cargo build` 能成功完成;如失败则根据错误最小化修改并重试,直到构建通过为止。",
1169
+ f"- crate_dir: {created_dir}",
1170
+ f"- crate_name: {crate_pkg_name}",
1171
+ "目录结构(部分):",
1172
+ dir_ctx,
1173
+ "",
1174
+ "执行与修复流程(务必按序执行,可多轮迭代):",
1175
+ "1) 先补齐 Rust 模块声明(仅最小化追加/升级,不覆盖业务实现):",
1176
+ " - 扫描 src 目录:",
1177
+ " * 在每个子目录下(除 src 根)创建或更新 mod.rs,仅追加缺失的 `pub mod <child>;` 声明;",
1178
+ " * 在 src/lib.rs 中为顶级子模块追加 `pub mod <name>;`;不要创建 src/mod.rs;忽略 lib.rs 与 main.rs 的自引用;",
1179
+ " - 若存在 `mod <name>;` 但非 pub,则就地升级为 `pub mod <name>;`,保留原缩进与其他内容;",
1180
+ " - 严禁删除现有声明或修改非声明代码;",
1181
+ '2) 在 Cargo.toml 的 [package] 中设置 edition:"2024";若本地工具链不支持 2024,请降级为 "2021" 并在说明中记录原因;保留其他已有字段与依赖不变。',
1182
+ "3) 根据当前源代码实际情况配置入口:",
1183
+ " - 仅库:仅配置 [lib](path=src/lib.rs),不要生成 main.rs;",
1184
+ " - 单一可执行:存在 src/main.rs 时配置 [[bin]] 或默认二进制;可选保留 [lib] 以沉淀共享逻辑;",
1185
+ " - 多可执行:为每个 src/bin/<name>.rs 配置 [[bin]];共享代码放在 src/lib.rs;",
1186
+ " - 不要创建与目录结构不一致的占位入口。",
1187
+ "4) 对被作为入口的源文件:若不存在 fn main() 则仅添加最小可用实现(不要改动已存在的实现):",
1188
+ ' fn main() { println!("ok"); }',
1189
+ "5) 执行一次构建验证:`cargo build -q`(或 `cargo check -q`)。",
1190
+ "6) 若构建失败,读取错误并进行最小化修复,然后再次构建;重复直至成功。仅允许的修复类型:",
1191
+ " - 依赖缺失:在 [dependencies] 中添加必要且稳定版本的依赖(优先无特性),避免新增未使用依赖;",
1192
+ " - 入口/crate-type 配置错误:修正 [lib] 或 [[bin]] 的 name/path/crate-type 使之与目录与入口文件一致;",
1193
+ " - 语言/工具链不兼容:将 edition 从 2024 调整为 2021;必要时可添加 rust-version 要求;",
1194
+ " - 语法级/最小实现缺失:仅在入口文件中补充必要的 use/空实现/feature gate 以通过编译,避免改动非入口业务文件;",
1195
+ " - 不要删除或移动现有文件与目录。",
1196
+ "7) 每轮修改后必须运行 `cargo build -q` 验证,直到构建成功为止。",
1197
+ "",
1198
+ "修改约束:",
1199
+ "- 允许修改的文件范围:Cargo.toml、src/lib.rs、src/main.rs、src/bin/*.rs、src/**/mod.rs(仅最小必要变更);除非为修复构建与模块声明补齐,不要修改其他文件。",
1200
+ "- 尽量保持现有内容与结构不变,不要引入与构建无关的改动或格式化。",
1201
+ "",
1202
+ "交付要求:",
1203
+ "- 以补丁方式提交实际修改的文件;",
1204
+ "- 在最终回复中简要说明所做变更与最终 `cargo build` 的结果(成功/失败及原因)。",
1205
+ ]
1206
+ requirement_text = "\n".join(requirement_lines)
1207
+
1208
+ prev_cwd = os.getcwd()
1209
+ try:
1210
+ # 切换到 crate 目录运行 CodeAgent 与构建
1211
+ os.chdir(str(created_dir))
1212
+ print(f"[c2rust-llm-planner] 已切换到 crate 目录: {os.getcwd()},执行 CodeAgent 初始化")
1213
+ agent = CodeAgent(need_summary=False, non_interactive=True, plan=False, model_group=llm_group)
1214
+ agent.run(requirement_text, prefix="[c2rust-llm-planner]", suffix="")
1215
+ print("[c2rust-llm-planner] 初始 CodeAgent 运行完成。")
1216
+
1217
+ # 进入构建与修复循环:构建失败则生成新的 CodeAgent,携带错误上下文进行最小修复
1218
+ iter_count = 0
1219
+ while True:
1220
+ iter_count += 1
1221
+ print(f"[c2rust-llm-planner] 在 {os.getcwd()} 执行: cargo build -q")
1222
+ build_res = subprocess.run(
1223
+ ["cargo", "build", "-q"],
1224
+ capture_output=True,
1225
+ text=True,
1226
+ check=False,
1227
+ )
1228
+ stdout = build_res.stdout or ""
1229
+ stderr = build_res.stderr or ""
1230
+ output = (stdout + "\n" + stderr).strip()
1231
+
1232
+ if build_res.returncode == 0:
1233
+ print("[c2rust-llm-planner] Cargo 构建成功。")
1234
+ break
1235
+
1236
+ print(f"[c2rust-llm-planner] Cargo 构建失败 (iter={iter_count})。")
1237
+ # 打印编译错误输出,便于可视化与调试
1238
+ print("[c2rust-llm-planner] 构建错误输出:")
1239
+ print(output)
1240
+ # 将错误信息作为上下文,附加修复原则,生成新的 CodeAgent 进行最小修复
1241
+ repair_prompt = "\n".join([
1242
+ requirement_text,
1243
+ "",
1244
+ "请根据以下构建错误进行最小化修复,然后再次执行 `cargo build` 验证:",
1245
+ "<BUILD_ERROR>",
1246
+ output,
1247
+ "</BUILD_ERROR>",
1248
+ ])
1249
+
1250
+ repair_agent = CodeAgent(need_summary=False, non_interactive=True, plan=False, model_group=llm_group)
1251
+ repair_agent.run(repair_prompt, prefix=f"[c2rust-llm-planner][iter={iter_count}]", suffix="")
1252
+ # 不切换目录,保持在原始工作目录
1253
+ finally:
1254
+ # 恢复之前的工作目录
1255
+ os.chdir(prev_cwd)
1256
+
1257
+ # 3) 输出 YAML 到文件(如指定),并返回解析后的 entries
1258
+ if out is not None:
1259
+ out_path = Path(out)
1260
+ out_path.parent.mkdir(parents=True, exist_ok=True)
1261
+ # 使用原始文本写出,便于可读
1262
+ out_path.write_text(yaml_text, encoding="utf-8")
1263
+ print(f"[c2rust-llm-planner] YAML 已写入: {out_path}")
1264
+
1265
+ return entries