1use std::collections::HashMap;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12use super::incremental_updater::{IncrementalBlueprintUpdater, UpdateOptions};
13use super::types_chunked::*;
14
15#[derive(Debug, Clone, Default)]
17pub struct SyncOptions {
18 pub verbose: bool,
20 pub on_progress: Option<fn(&str)>,
22}
23
24#[derive(Debug, Clone)]
26pub struct SyncResult {
27 pub success: bool,
29 pub message: String,
31 pub synced_files: Vec<String>,
33 pub conflicts: Vec<Conflict>,
35}
36
37#[derive(Debug, Clone)]
39pub struct Conflict {
40 pub conflict_type: ConflictType,
42 pub module_id: String,
44 pub expected: Vec<String>,
46 pub actual: Vec<String>,
48 pub resolution: ConflictResolution,
50 pub description: String,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum ConflictType {
57 ExportMismatch,
59 StructureChange,
61 ContentDiverged,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum ConflictResolution {
68 UseBlueprint,
70 UseCode,
72 Manual,
74}
75
76#[derive(Debug, Clone)]
78pub struct CodeGenerationResult {
79 pub success: bool,
81 pub file_path: Option<String>,
83 pub code: Option<String>,
85 pub error: Option<String>,
87}
88
89pub struct BlueprintCodeSyncManager {
91 root_path: PathBuf,
92 map_dir: PathBuf,
93 chunks_dir: PathBuf,
94 index_path: PathBuf,
95 updater: IncrementalBlueprintUpdater,
96}
97
98impl BlueprintCodeSyncManager {
99 pub fn new(root_path: impl AsRef<Path>) -> Self {
101 let root = root_path.as_ref().to_path_buf();
102 let map_dir = root.join(".claude").join("map");
103 let chunks_dir = map_dir.join("chunks");
104 let index_path = map_dir.join("index.json");
105
106 Self {
107 root_path: root.clone(),
108 map_dir,
109 chunks_dir,
110 index_path,
111 updater: IncrementalBlueprintUpdater::new(root),
112 }
113 }
114
115 pub fn sync_code_to_blueprint(
121 &mut self,
122 changed_files: &[String],
123 options: &SyncOptions,
124 ) -> SyncResult {
125 let mut conflicts = Vec::new();
126 let mut synced_files = Vec::new();
127
128 self.log(
129 options,
130 &format!("开始同步 {} 个文件到蓝图...", changed_files.len()),
131 );
132
133 for file in changed_files {
134 let design = self.get_module_design(file);
136
137 if let Some(ref d) = design {
139 if d.status == PlannedStatus::Planned {
140 let code_path = self.root_path.join(file);
141 if code_path.exists() {
142 self.update_module_status(file, ModuleStatus::Implemented);
144 self.log(options, &format!(" ✓ {}: planned → implemented", file));
145 }
146 }
147 }
148
149 if let Some(conflict) = self.detect_conflict(file, &design) {
151 conflicts.push(conflict);
152 self.log(options, &format!(" ⚠ {}: 检测到冲突", file));
153 }
154
155 synced_files.push(file.clone());
156 }
157
158 let update_options = UpdateOptions {
160 files: Some(changed_files.to_vec()),
161 verbose: options.verbose,
162 on_progress: options.on_progress,
163 ..Default::default()
164 };
165 let _ = self.updater.update(&update_options);
166
167 SyncResult {
168 success: true,
169 message: format!(
170 "已同步 {} 个文件,{} 个冲突",
171 synced_files.len(),
172 conflicts.len()
173 ),
174 synced_files,
175 conflicts,
176 }
177 }
178
179 pub fn sync_blueprint_to_code(
185 &mut self,
186 module_id: &str,
187 options: &SyncOptions,
188 ) -> CodeGenerationResult {
189 self.log(options, &format!("正在从蓝图生成代码: {}...", module_id));
190
191 let design = match self.get_module_design(module_id) {
193 Some(d) => d,
194 None => {
195 return CodeGenerationResult {
196 success: false,
197 file_path: None,
198 code: None,
199 error: Some(format!("未找到模块设计: {}", module_id)),
200 };
201 }
202 };
203
204 if design.status == PlannedStatus::InProgress {
206 }
208
209 let code = self.generate_code_from_design(module_id, &design);
211
212 let target_path = self.root_path.join(module_id);
214 if let Some(parent) = target_path.parent() {
215 let _ = fs::create_dir_all(parent);
216 }
217
218 if target_path.exists() {
220 return CodeGenerationResult {
221 success: false,
222 file_path: None,
223 code: None,
224 error: Some(format!(
225 "文件已存在: {}。请先删除现有文件或更新蓝图状态。",
226 module_id
227 )),
228 };
229 }
230
231 if let Err(e) = fs::write(&target_path, &code) {
233 return CodeGenerationResult {
234 success: false,
235 file_path: None,
236 code: None,
237 error: Some(format!("写入文件失败: {}", e)),
238 };
239 }
240
241 self.update_module_status(module_id, ModuleStatus::InProgress);
243
244 self.log(options, &format!(" ✓ 已生成: {}", module_id));
245
246 CodeGenerationResult {
247 success: true,
248 file_path: Some(target_path.to_string_lossy().to_string()),
249 code: Some(code),
250 error: None,
251 }
252 }
253
254 pub fn sync_all_planned_modules(&mut self, options: &SyncOptions) -> SyncResult {
256 let planned_modules = self.get_all_planned_modules();
257 let mut synced_files = Vec::new();
258 let mut conflicts = Vec::new();
259
260 self.log(
261 options,
262 &format!("找到 {} 个计划模块", planned_modules.len()),
263 );
264
265 for module in planned_modules {
266 let result = self.sync_blueprint_to_code(&module.id, options);
267
268 if result.success {
269 synced_files.push(module.id.clone());
270 } else if let Some(ref error) = result.error {
271 if error.contains("已存在") {
272 conflicts.push(Conflict {
273 conflict_type: ConflictType::ContentDiverged,
274 module_id: module.id,
275 expected: vec!["planned".to_string()],
276 actual: vec!["file-exists".to_string()],
277 resolution: ConflictResolution::Manual,
278 description: error.clone(),
279 });
280 }
281 }
282 }
283
284 SyncResult {
285 success: conflicts.is_empty(),
286 message: format!(
287 "已生成 {} 个文件,{} 个冲突",
288 synced_files.len(),
289 conflicts.len()
290 ),
291 synced_files,
292 conflicts,
293 }
294 }
295
296 fn detect_conflict(&self, module_id: &str, design: &Option<PlannedModule>) -> Option<Conflict> {
302 let design = design.as_ref()?;
303
304 let code_path = self.root_path.join(module_id);
305 if !code_path.exists() {
306 return None;
307 }
308
309 let code = fs::read_to_string(&code_path).ok()?;
311
312 let actual_exports = self.extract_exports(&code);
314
315 let expected_exports = design.expected_exports.as_ref()?;
317
318 if !expected_exports.is_empty() {
319 let missing: Vec<_> = expected_exports
320 .iter()
321 .filter(|e| !actual_exports.contains(e))
322 .cloned()
323 .collect();
324 let extra: Vec<_> = actual_exports
325 .iter()
326 .filter(|e| !expected_exports.contains(e))
327 .cloned()
328 .collect();
329
330 if !missing.is_empty() || !extra.is_empty() {
331 return Some(Conflict {
332 conflict_type: ConflictType::ExportMismatch,
333 module_id: module_id.to_string(),
334 expected: expected_exports.clone(),
335 actual: actual_exports,
336 resolution: ConflictResolution::Manual,
337 description: format!(
338 "导出不匹配。缺少: {};多余: {}",
339 missing.join(", "),
340 extra.join(", ")
341 ),
342 });
343 }
344 }
345
346 None
347 }
348
349 fn extract_exports(&self, code: &str) -> Vec<String> {
351 let mut exports = Vec::new();
352
353 let patterns = [
355 r"pub\s+struct\s+(\w+)",
356 r"pub\s+fn\s+(\w+)",
357 r"pub\s+const\s+(\w+)",
358 r"pub\s+enum\s+(\w+)",
359 r"pub\s+trait\s+(\w+)",
360 r"pub\s+type\s+(\w+)",
361 r"export\s+(?:default\s+)?class\s+(\w+)",
363 r"export\s+(?:default\s+)?function\s+(\w+)",
364 r"export\s+(?:const|let|var)\s+(\w+)",
365 r"export\s+interface\s+(\w+)",
366 r"export\s+type\s+(\w+)",
367 r"export\s+enum\s+(\w+)",
368 ];
369
370 for pattern in patterns {
371 if let Ok(re) = regex::Regex::new(pattern) {
372 for cap in re.captures_iter(code) {
373 if let Some(name) = cap.get(1) {
374 exports.push(name.as_str().to_string());
375 }
376 }
377 }
378 }
379
380 exports.sort();
381 exports.dedup();
382 exports
383 }
384
385 fn get_module_design(&self, module_id: &str) -> Option<PlannedModule> {
391 let dir_path = Path::new(module_id)
392 .parent()
393 .map(|p| p.to_string_lossy().to_string())
394 .unwrap_or_default();
395 let dir_path = if dir_path == "." {
396 String::new()
397 } else {
398 dir_path
399 };
400
401 let chunk_file_name = self.get_chunk_file_name(&dir_path);
402 let chunk_path = self.chunks_dir.join(&chunk_file_name);
403
404 if !chunk_path.exists() {
405 return None;
406 }
407
408 let content = fs::read_to_string(&chunk_path).ok()?;
409 let chunk: ChunkData = serde_json::from_str(&content).ok()?;
410
411 if let Some(ref planned_modules) = chunk.planned_modules {
413 if let Some(planned) = planned_modules.iter().find(|m| m.id == module_id) {
414 return Some(planned.clone());
415 }
416 }
417
418 None
419 }
420
421 fn update_module_status(&self, module_id: &str, status: ModuleStatus) {
423 let dir_path = Path::new(module_id)
424 .parent()
425 .map(|p| p.to_string_lossy().to_string())
426 .unwrap_or_default();
427 let dir_path = if dir_path == "." {
428 String::new()
429 } else {
430 dir_path
431 };
432
433 let chunk_file_name = self.get_chunk_file_name(&dir_path);
434 let chunk_path = self.chunks_dir.join(&chunk_file_name);
435
436 if !chunk_path.exists() {
437 return;
438 }
439
440 let content = match fs::read_to_string(&chunk_path) {
441 Ok(c) => c,
442 Err(_) => return,
443 };
444
445 let mut chunk: ChunkData = match serde_json::from_str(&content) {
446 Ok(c) => c,
447 Err(_) => return,
448 };
449
450 if status == ModuleStatus::Implemented {
452 if let Some(ref mut planned_modules) = chunk.planned_modules {
453 if let Some(pos) = planned_modules.iter().position(|m| m.id == module_id) {
454 let planned = planned_modules.remove(pos);
455
456 let meta = chunk.module_design_meta.get_or_insert_with(HashMap::new);
458 meta.insert(
459 module_id.to_string(),
460 ModuleDesignMeta {
461 status: Some(ModuleStatus::Implemented),
462 design_notes: Some(planned.design_notes),
463 marked_at: Some(chrono::Utc::now().to_rfc3339()),
464 },
465 );
466 }
467 }
468 } else {
469 let meta = chunk.module_design_meta.get_or_insert_with(HashMap::new);
471 if let Some(existing) = meta.get_mut(module_id) {
472 existing.status = Some(status);
473 existing.marked_at = Some(chrono::Utc::now().to_rfc3339());
474 } else {
475 meta.insert(
476 module_id.to_string(),
477 ModuleDesignMeta {
478 status: Some(status),
479 design_notes: None,
480 marked_at: Some(chrono::Utc::now().to_rfc3339()),
481 },
482 );
483 }
484 }
485
486 if let Ok(json) = serde_json::to_string_pretty(&chunk) {
488 let _ = fs::write(&chunk_path, json);
489 }
490 }
491
492 fn get_all_planned_modules(&self) -> Vec<PlannedModule> {
494 let mut planned_modules = Vec::new();
495
496 if !self.chunks_dir.exists() {
497 return planned_modules;
498 }
499
500 if let Ok(entries) = fs::read_dir(&self.chunks_dir) {
501 for entry in entries.flatten() {
502 let path = entry.path();
503 if path.extension().is_some_and(|e| e == "json") {
504 if let Ok(content) = fs::read_to_string(&path) {
505 if let Ok(chunk) = serde_json::from_str::<ChunkData>(&content) {
506 if let Some(modules) = chunk.planned_modules {
507 for module in modules {
508 if module.status == PlannedStatus::Planned
509 || module.status == PlannedStatus::InProgress
510 {
511 planned_modules.push(module);
512 }
513 }
514 }
515 }
516 }
517 }
518 }
519 }
520
521 planned_modules
522 }
523
524 fn generate_code_from_design(&self, module_id: &str, design: &PlannedModule) -> String {
526 let name = Path::new(module_id)
527 .file_stem()
528 .map(|s| s.to_string_lossy().to_string())
529 .unwrap_or_else(|| "module".to_string());
530 let struct_name = self.to_pascal_case(&name);
531
532 let design_notes = &design.design_notes;
534
535 let dependencies = &design.dependencies;
537
538 let mut imports = String::new();
540 for dep in dependencies {
541 let dep_name = Path::new(dep)
542 .file_stem()
543 .map(|s| s.to_string_lossy().to_string())
544 .unwrap_or_default();
545 imports.push_str(&format!("// use crate::{}::*; // TODO\n", dep_name));
546 }
547
548 let expected_exports = design
550 .expected_exports
551 .clone()
552 .unwrap_or_else(|| vec![struct_name.clone()]);
553
554 let is_rust = module_id.ends_with(".rs");
556 let is_typescript = module_id.ends_with(".ts") || module_id.ends_with(".tsx");
557
558 if is_rust {
559 self.generate_rust_code(
560 &name,
561 &struct_name,
562 design_notes,
563 &imports,
564 &expected_exports,
565 )
566 } else if is_typescript {
567 self.generate_typescript_code(
568 &name,
569 &struct_name,
570 design_notes,
571 &imports,
572 &expected_exports,
573 )
574 } else {
575 self.generate_rust_code(
576 &name,
577 &struct_name,
578 design_notes,
579 &imports,
580 &expected_exports,
581 )
582 }
583 }
584
585 fn generate_rust_code(
587 &self,
588 name: &str,
589 struct_name: &str,
590 design_notes: &str,
591 imports: &str,
592 expected_exports: &[String],
593 ) -> String {
594 let other_exports: String = expected_exports
595 .iter()
596 .filter(|e| *e != struct_name)
597 .map(|e| {
598 format!(
599 "\n/// {}\n/// TODO: 实现\npub const {}: () = ();\n",
600 e,
601 e.to_uppercase()
602 )
603 })
604 .collect();
605
606 format!(
607 r#"//! {}
608//!
609//! {}
610//!
611//! @module {}
612//! @created {}
613//! @status in-progress
614
615{}
616/// {}
617///
618/// 设计说明:
619/// {}
620pub struct {} {{
621 // TODO: 添加字段
622}}
623
624impl {} {{
625 /// 创建新实例
626 pub fn new() -> Self {{
627 Self {{
628 // TODO: 初始化
629 }}
630 }}
631
632 // TODO: 实现方法
633}}
634
635impl Default for {} {{
636 fn default() -> Self {{
637 Self::new()
638 }}
639}}
640{}
641"#,
642 name,
643 design_notes,
644 name,
645 chrono::Utc::now().format("%Y-%m-%d"),
646 imports,
647 struct_name,
648 design_notes.replace('\n', "\n/// "),
649 struct_name,
650 struct_name,
651 struct_name,
652 other_exports
653 )
654 }
655
656 fn generate_typescript_code(
658 &self,
659 name: &str,
660 class_name: &str,
661 design_notes: &str,
662 imports: &str,
663 expected_exports: &[String],
664 ) -> String {
665 let other_exports: String = expected_exports
666 .iter()
667 .filter(|e| *e != class_name)
668 .map(|e| {
669 format!(
670 "\n/**\n * {}\n * TODO: 实现\n */\nexport const {} = undefined;\n",
671 e, e
672 )
673 })
674 .collect();
675
676 format!(
677 r#"/**
678 * {}
679 *
680 * {}
681 *
682 * @module {}
683 * @created {}
684 * @status in-progress
685 */
686
687{}
688/**
689 * {}
690 *
691 * 设计说明:
692 * {}
693 */
694export class {} {{
695 constructor() {{
696 // TODO: 初始化
697 }}
698
699 // TODO: 实现方法
700}}
701{}
702export default {};
703"#,
704 name,
705 design_notes,
706 name,
707 chrono::Utc::now().format("%Y-%m-%d"),
708 imports,
709 class_name,
710 design_notes.replace('\n', "\n * "),
711 class_name,
712 other_exports,
713 class_name
714 )
715 }
716
717 fn to_pascal_case(&self, s: &str) -> String {
719 s.split(['-', '_'])
720 .map(|word| {
721 let mut chars = word.chars();
722 match chars.next() {
723 None => String::new(),
724 Some(first) => first.to_uppercase().chain(chars).collect(),
725 }
726 })
727 .collect()
728 }
729
730 fn get_chunk_file_name(&self, dir_path: &str) -> String {
732 if dir_path.is_empty() || dir_path == "." {
733 "root.json".to_string()
734 } else {
735 format!("{}.json", dir_path.replace(['/', '\\'], "_"))
736 }
737 }
738
739 fn log(&self, options: &SyncOptions, message: &str) {
741 if options.verbose {
742 if let Some(callback) = options.on_progress {
743 callback(message);
744 } else {
745 println!("{}", message);
746 }
747 }
748 }
749}
750
751pub fn sync_code_to_blueprint(
757 root_path: impl AsRef<Path>,
758 changed_files: &[String],
759 options: &SyncOptions,
760) -> SyncResult {
761 let mut manager = BlueprintCodeSyncManager::new(root_path);
762 manager.sync_code_to_blueprint(changed_files, options)
763}
764
765pub fn sync_blueprint_to_code(
767 root_path: impl AsRef<Path>,
768 module_id: &str,
769 options: &SyncOptions,
770) -> CodeGenerationResult {
771 let mut manager = BlueprintCodeSyncManager::new(root_path);
772 manager.sync_blueprint_to_code(module_id, options)
773}